java并發(fā)編程系列:生產(chǎn)者-消費者模式

目光從廁所轉(zhuǎn)到飯館,一個飯館里通常都有好多廚師以及好多服務(wù)員留搔,這里我們把廚師稱為生產(chǎn)者更胖,把服務(wù)員稱為消費者,廚師和服務(wù)員是不直接打交道的隔显,而是在廚師做好菜之后放到窗口却妨,服務(wù)員從窗口直接把菜端走給客人就好了,這樣會極大的提升工作效率括眠,因為省去了生產(chǎn)者和消費者之間的溝通成本管呵。從java的角度看這個事情,每一個廚師就相當于一個生產(chǎn)者線程哺窄,每一個服務(wù)員都相當于一個消費者線程捐下,而放菜的窗口就相當于一個緩沖隊列,生產(chǎn)者線程不斷把生產(chǎn)好的東西放到緩沖隊列里萌业,消費者線程不斷從緩沖隊列里取東西坷襟,畫個圖就像是這樣:


現(xiàn)實中放菜的窗口能放的菜數(shù)量是有限的,我們假設(shè)這個窗口只能放5個菜生年。那么廚師在做完菜之后需要看一下窗口是不是滿了婴程,如果窗口已經(jīng)滿了的話,就在一旁抽根煙等待抱婉,直到有服務(wù)員來取菜的時候通知一下廚師窗口有了空閑档叔,可以放菜了,這時廚師再把自己做的菜放到窗口上去炒下一個菜蒸绩。從服務(wù)員的角度來說衙四,如果窗口是空的,那么也去一旁抽根煙等待患亿,直到有廚師把菜做好了放到窗口上传蹈,并且通知他們一下,然后再把菜端走步藕。
我們先用java抽象一下菜:

public class Food {
    private static int counter = 0;

    private int i;  //代表生產(chǎn)的第幾個菜

    public Food() {
        i = ++counter;
    }

    @Override
    public String toString() {
        return "第" + i + "個菜";
    }
}

每次創(chuàng)建Food對象惦界,字段i的值都會加1,代表這是創(chuàng)建的第幾道菜咙冗。

為了故事的順利進行沾歪,我們首先定義一個工具類:

class SleepUtil {

    private static Random random = new Random();
    
    public static void randomSleep() {
        try {
            Thread.sleep(random.nextInt(1000));
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

SleepUtil的靜態(tài)方法randomSleep代表當前線程隨機休眠一秒內(nèi)的時間。

然后我們再用java定義一下廚師:

public class Cook extends Thread {

    private Queue<Food> queue;

    public Cook(Queue<Food> queue, String name) {
        super(name);
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true) {
            SleepUtil.randomSleep();    //模擬廚師炒菜時間
            Food food = new Food();
            System.out.println(getName() + " 生產(chǎn)了" + food);
            synchronized (queue) {
                while (queue.size() > 4) {
                    try {
                        System.out.println("隊列元素超過5個雾消,為:" + queue.size() + " " + getName() + "抽根煙等待中");
                        queue.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                queue.add(food);
                queue.notifyAll();
            }
        }
    }
}

我們說每一個廚師Cook都是一個線程灾搏,內(nèi)部維護了一個名叫queue的隊列挫望。在run方法中是一個死循環(huán),代表不斷的生產(chǎn)Food确镊。他每生產(chǎn)一個Food后,都要判斷queue隊列中元素的個數(shù)是不是大于4范删,如果大于4的話蕾域,就調(diào)用queue.wait()等待,如果不大于4的話到旦,就把創(chuàng)建號的Food對象放到queue隊列中旨巷,由于可能多個線程同時訪問queue的各個方法,所以對這段代碼用queue對象來加鎖保護添忘。當向隊列添加完剛創(chuàng)建的Food對象之后采呐,就可以通知queue這個鎖對象關(guān)聯(lián)的等待隊列中的服務(wù)員線程們可以繼續(xù)端菜了。

然后我們再用java定義一下服務(wù)員:

class Waiter extends Thread {

    private Queue<Food> queue;

    public Waiter(Queue<Food> queue, String name) {
        super(name);
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true) {
            Food food;
            synchronized (queue) {
                while (queue.size() < 1) {
                    try {
                        System.out.println("隊列元素個數(shù)為: " + queue.size() + "搁骑," + getName() + "抽根煙等待中");
                        queue.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                food = queue.remove();
                System.out.println(getName() + " 獲取到:" + food);
                queue.notifyAll();
            }

            SleepUtil.randomSleep();    //模擬服務(wù)員端菜時間
        }
    }
}

每個服務(wù)員也是一個線程斧吐,和廚師一樣,都在內(nèi)部維護了一個名叫queue的隊列仲器。在run方法中是一個死循環(huán)煤率,代表不斷的從隊列中取走Food。每次在從queue隊列中取Food對象的時候乏冀,都需要判斷一下隊列中的元素是否小于1蝶糯,如果小于1的話,就調(diào)用queue.wait()等待辆沦,如果不小于1的話昼捍,也就是隊列里有元素,就從隊列里取走一個Food對象肢扯,并且通知與queue這個鎖對象關(guān)聯(lián)的等待隊列中的廚師線程們可以繼續(xù)向隊列里放入Food對象了妒茬。
在廚師和服務(wù)員線程類都定義好了之后,我們再創(chuàng)建一個Restaurant類蔚晨,來看看在餐館里真實發(fā)生的事情:

public class Restaurant {

    public static void main(String[] args) {

        Queue<Food> queue = new LinkedList<>();
        new Cook(queue, "1號廚師").start();
        new Cook(queue, "2號廚師").start();
        new Cook(queue, "3號廚師").start();
        new Waiter(queue, "1號服務(wù)員").start();
        new Waiter(queue, "2號服務(wù)員").start();
        new Waiter(queue, "3號服務(wù)員").start();
    }
}

我們在Restaurant中安排了3個廚師和3個服務(wù)員郊闯,大家執(zhí)行一下這個程序,會發(fā)現(xiàn)在如果廚師生產(chǎn)的過快蛛株,廚師就會等待团赁,如果服務(wù)員端菜速度過快,服務(wù)員就會等待谨履。但是整個過程廚師和服務(wù)員是沒有任何關(guān)系的欢摄,它們是通過隊列queue實現(xiàn)了所謂的解耦。

這個過程雖然不是很復(fù)雜笋粟,但是使用中還是需要注意一些問題:

  • 我們這里的廚師和服務(wù)員使用同一個鎖queue怀挠。

使用同一個鎖是因為對queue的操作只能用同一個鎖來保護析蝴,假設(shè)使用不同的鎖,廚師線程調(diào)用queue.add方法绿淋,服務(wù)員線程調(diào)用queue.remove方法闷畸,這兩個方法都不是原子操作,多線程并發(fā)執(zhí)行的時候會出現(xiàn)不可預(yù)測的結(jié)果吞滞,所以我們使用同一個鎖來保護對queue這個變量的操作佑菩,這一點我們在嘮叨設(shè)計線程安全類的時候已經(jīng)強調(diào)過了。

  • 廚師和服務(wù)員線程使用同一個鎖queue的后果就是廚師線程和服務(wù)員線程使用的是同一個等待隊列裁赠。

但是同一時刻廚師線程和服務(wù)員線程不會同時在等待隊列中殿漠,因為當廚師線程在wait的時候,隊列里的元素肯定是5佩捞,此時服務(wù)員線程肯定是不會wait的绞幌,但是消費的過程是被鎖對象queue保護的,所以在一個服務(wù)員線程消費了一個Food之后一忱,就會調(diào)用notifyAll來喚醒等待隊列中的廚師線程們莲蜘;當消費者線程在wait的時候,隊列里的元素肯定是0帘营,此時廚師線程肯定是不會wait的菇夸,生產(chǎn)的過程是被鎖對象queue保護的,所以在一個廚師線程生產(chǎn)了一個Food對象之后仪吧,就會調(diào)用notifyAll來喚醒等待隊列中的服務(wù)員線程們庄新。所以同一時刻廚師線程和服務(wù)員線程不會同時在等待隊列中。

  • 在生產(chǎn)和消費過程薯鼠,我們都調(diào)用了SleepUtil.randomSleep();择诈。

我們這里的生產(chǎn)者-消費者模型是把實際使用的場景進行了簡化,真正的實際場景中生產(chǎn)過程和消費過程一般都會很耗時出皇,這些耗時的操作最好不要放在同步代碼塊中羞芍,這樣會造成別的線程的長時間阻塞。如果把生產(chǎn)過程和消費過程都放在同步代碼塊中郊艘,也就是說在一個廚師炒菜的同時不允許別的廚師炒菜荷科,在一個服務(wù)員端菜的同時不允許別的服務(wù)員端菜,這個顯然是不合理的纱注,大家需要注意這一點畏浆。

以上就是wait/notify機制的一個現(xiàn)實應(yīng)用:生產(chǎn)者-消費者模式的一個簡介。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末狞贱,一起剝皮案震驚了整個濱河市刻获,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌瞎嬉,老刑警劉巖蝎毡,帶你破解...
    沈念sama閱讀 211,561評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件厚柳,死亡現(xiàn)場離奇詭異,居然都是意外死亡沐兵,警方通過查閱死者的電腦和手機别垮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來扎谎,“玉大人碳想,你說我怎么就攤上這事〔就福” “怎么了移袍?”我有些...
    開封第一講書人閱讀 157,162評論 0 348
  • 文/不壞的土叔 我叫張陵解藻,是天一觀的道長老充。 經(jīng)常有香客問我,道長螟左,這世上最難降的妖魔是什么啡浊? 我笑而不...
    開封第一講書人閱讀 56,470評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮胶背,結(jié)果婚禮上巷嚣,老公的妹妹穿的比我還像新娘。我一直安慰自己钳吟,他們只是感情好廷粒,可當我...
    茶點故事閱讀 65,550評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著红且,像睡著了一般坝茎。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上暇番,一...
    開封第一講書人閱讀 49,806評論 1 290
  • 那天嗤放,我揣著相機與錄音,去河邊找鬼壁酬。 笑死次酌,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的舆乔。 我是一名探鬼主播岳服,決...
    沈念sama閱讀 38,951評論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼希俩!你這毒婦竟也來了派阱?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,712評論 0 266
  • 序言:老撾萬榮一對情侶失蹤斜纪,失蹤者是張志新(化名)和其女友劉穎贫母,沒想到半個月后文兑,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,166評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡腺劣,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,510評論 2 327
  • 正文 我和宋清朗相戀三年绿贞,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片橘原。...
    茶點故事閱讀 38,643評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡籍铁,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出趾断,到底是詐尸還是另有隱情拒名,我是刑警寧澤,帶...
    沈念sama閱讀 34,306評論 4 330
  • 正文 年R本政府宣布芋酌,位于F島的核電站增显,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏脐帝。R本人自食惡果不足惜同云,卻給世界環(huán)境...
    茶點故事閱讀 39,930評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望堵腹。 院中可真熱鬧炸站,春花似錦、人聲如沸疚顷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,745評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽腿堤。三九已至阀坏,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間释液,已是汗流浹背全释。 一陣腳步聲響...
    開封第一講書人閱讀 31,983評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留误债,地道東北人浸船。 一個月前我還...
    沈念sama閱讀 46,351評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像寝蹈,于是被迫代替她去往敵國和親李命。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,509評論 2 348

推薦閱讀更多精彩內(nèi)容