目光從廁所轉(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)者-消費者模式的一個簡介。