標籤: 「咱們都是小青蛙」公衆號文章java
目光從廁所轉到飯館,一個飯館裏一般都有好多廚師以及好多服務員,這裏咱們把廚師稱爲生產者,把服務員稱爲消費者,廚師和服務員是不直接打交道的,而是在廚師作好菜以後放到窗口,服務員從窗口直接把菜端走給客人就行了,這樣會極大的提高工做效率,由於省去了生產者和消費者之間的溝通成本。從java的角度看這個事情,每個廚師就至關於一個生產者
線程,每個服務員都至關於一個消費者
線程,而放菜的窗口就至關於一個緩衝隊列
,生產者線程不斷把生產好的東西放到緩衝隊列裏,消費者線程不斷從緩衝隊列裏取東西,畫個圖就像是這樣:程序員
現實中放菜的窗口能放的菜數量是有限的,咱們假設這個窗口只能放5個菜。那麼廚師在作完菜以後須要看一下窗口是否是滿了,若是窗口已經滿了的話,就在一旁抽根菸等待
,直到有服務員來取菜的時候通知
一下廚師窗口有了空閒,能夠放菜了,這時廚師再把本身作的菜放到窗口上去炒下一個菜。從服務員的角度來講,若是窗口是空的,那麼也去一旁抽根菸等待
,直到有廚師把菜作好了放到窗口上,而且通知
他們一下,而後再把菜端走。安全
咱們先用java抽象一下菜
:多線程
public class Food {
private static int counter = 0;
private int i; //表明生產的第幾個菜
public Food() {
i = ++counter;
}
@Override
public String toString() {
return "第" + i + "個菜";
}
}
複製代碼
每次建立Food
對象,字段i
的值都會加1,表明這是建立的第幾道菜。併發
爲了故事的順利進行,咱們首先定義一個工具類:dom
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
的靜態方法randomSleep
表明當前線程隨機休眠一秒內的時間。ide
而後咱們再用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() + " 生產了" + 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
都是一個線程,內部維護了一個名叫queue
的隊列。在run
方法中是一個死循環,表明不斷的生產Food
。他每生產一個Food
後,都要判斷queue
隊列中元素的個數是否是大於4,若是大於4的話,就調用queue.wait()
等待,若是不大於4的話,就把建立號的Food
對象放到queue
隊列中,因爲可能多個線程同時訪問queue
的各個方法,因此對這段代碼用queue
對象來加鎖保護。當向隊列添加完剛建立的Food
對象以後,就能夠通知queue
這個鎖對象關聯的等待隊列中的服務員線程們能夠繼續端菜了。學習
而後咱們再用java定義一下服務員:優化
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("隊列元素個數爲: " + queue.size() + "," + getName() + "抽根菸等待中");
queue.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
food = queue.remove();
System.out.println(getName() + " 獲取到:" + food);
queue.notifyAll();
}
SleepUtil.randomSleep(); //模擬服務員端菜時間
}
}
}
複製代碼
每一個服務員也是一個線程,和廚師同樣,都在內部維護了一個名叫queue
的隊列。在run
方法中是一個死循環,表明不斷的從隊列中取走Food
。每次在從queue
隊列中取Food
對象的時候,都須要判斷一下隊列中的元素是否小於1,若是小於1的話,就調用queue.wait()
等待,若是不小於1的話,也就是隊列裏有元素,就從隊列裏取走一個Food
對象,而且通知與queue
這個鎖對象關聯的等待隊列中的廚師線程們能夠繼續向隊列裏放入Food
對象了。
在廚師和服務員線程類都定義好了以後,咱們再建立一個Restaurant
類,來看看在餐館裏真實發生的事情:
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號服務員").start();
new Waiter(queue, "2號服務員").start();
new Waiter(queue, "3號服務員").start();
}
}
複製代碼
咱們在Restaurant
中安排了3個廚師和3個服務員,你們執行一下這個程序,會發如今若是廚師生產的過快,廚師就會等待,若是服務員端菜速度過快,服務員就會等待。可是整個過程廚師和服務員是沒有任何關係的,它們是經過隊列queue
實現了所謂的解耦。
這個過程雖然不是很複雜,可是使用中仍是須要注意一些問題:
咱們這裏的廚師和服務員使用同一個鎖queue
。
使用同一個鎖是由於對queue
的操做只能用同一個鎖來保護,假設使用不一樣的鎖,廚師線程調用queue.add
方法,服務員線程調用queue.remove
方法,這兩個方法都不是原子操做,多線程併發執行的時候會出現不可預測的結果,因此咱們使用同一個鎖來保護對queue
這個變量的操做,這一點咱們在嘮叨設計線程安全類的時候已經強調過了。
廚師和服務員線程使用同一個鎖queue
的後果就是廚師線程和服務員線程使用的是同一個等待隊列。
可是同一時刻廚師線程和服務員線程不會同時在等待隊列中,由於當廚師線程在wait
的時候,隊列裏的元素確定是5,此時服務員線程確定是不會wait
的,可是消費的過程是被鎖對象queue
保護的,因此在一個服務員線程消費了一個Food
以後,就會調用notifyAll
來喚醒等待隊列中的廚師線程們;當消費者線程在wait
的時候,隊列裏的元素確定是0,此時廚師線程確定是不會wait
的,生產的過程是被鎖對象queue
保護的,因此在一個廚師線程生產了一個Food
對象以後,就會調用notifyAll
來喚醒等待隊列中的服務員線程們。因此同一時刻廚師線程和服務員線程不會同時在等待隊列中。
在生產和消費過程,咱們都調用了SleepUtil.randomSleep();
。
咱們這裏的生產者-消費者模型是把實際使用的場景進行了簡化,真正的實際場景中生產過程和消費過程通常都會很耗時,這些耗時的操做最好不要放在同步代碼塊中,這樣會形成別的線程的長時間阻塞。若是把生產過程和消費過程都放在同步代碼塊中,也就是說在一個廚師炒菜的同時不容許別的廚師炒菜,在一個服務員端菜的同時不容許別的服務員端菜,這個顯然是不合理的,你們須要注意這一點。
以上就是wait/notify
機制的一個現實應用:生產者-消費者
模式的一個簡介。
寫文章挺累的,有時候你以爲閱讀挺流暢的,那實際上是背後無數次修改的結果。若是你以爲不錯請幫忙轉發一下,萬分感謝~ 這裏是個人公衆號,裏邊有更多技術乾貨,時不時扯一下犢子,歡迎關注:
另外,做者還寫了一本MySQL小冊:《MySQL是怎樣運行的:從根兒上理解MySQL》的連接 。小冊的內容主要是從小白的角度出發,用比較通俗的語言講解關於MySQL進階的一些核心概念,好比記錄、索引、頁面、表空間、查詢優化、事務和鎖等,總共的字數大約是三四十萬字,配有上百幅原創插圖。主要是想下降普通程序員學習MySQL進階的難度,讓學習曲線更平滑一點~ 有在MySQL進階方面有疑惑的同窗能夠看一下: