生產者消費者問題也叫有限緩衝問題,是多線程同步的一個最最最經典的問題。這個問題描述的場景是對於一個有固定大小的緩衝區,同時共享給兩個線程去使用。而這兩個線程會分爲兩個角色,一個負責往這個緩衝區裏放入必定的數據,咱們叫他生產者。另外一個負責從緩衝區裏取數據,咱們叫他消費者。
這裏就會有兩個問題,第一個問題是生產者不可能無限制的放數據去緩衝區,由於緩衝區是有大小的,當緩衝區滿的時候,生產者就必須中止生產。第二個問題亦然,消費者也不可能無限制的從緩衝區去取數據,取數據的前提是緩衝區裏有數據,因此當緩衝區空的時候,消費者就必須中止生產。
這兩個問題看起來簡單,可是在實際編碼的時候仍是會有許多坑,稍不留意就會鑄成大錯。並且上面只是單個消費者生產者問題,實現應用中,還會遇到多生產多消費等更復雜的場景。這些問題下面會詳細敘述。html
經過上節的內容,咱們知道了什麼是生產者消費者問題。可是爲何會出現這種問題呢?
其實若是說『生產者消費者問題』,可能由於有了『問題』兩個字而顯得比較負面。我更喜歡稱之爲『生產者消費者模式』,就像咱們學的那些代碼設計模式同樣。他實際上是多線程狀況下的一種設計模式,是某些場景下久經考驗的最佳實踐。
那麼這種模式有哪些做用呢?
他的第一個好處是解耦。
舉個外賣的例子。在沒有美團、餓了麼以前,確定沒有如今這麼多滿大街跑的外賣小哥。你打電話點了一份外賣,一般都是老闆本身作菜本身送。你想像一下,老闆洗菜、切菜、作菜,作好以後再打包,而後拎着打包盒,騎個自行車,再滿小區找地址,最後送到你的手中。這裏就會出現幾個問題,第一,老闆挺不容易的,要會洗菜、切菜、作菜烹飪一條龍,作好以後,還要會騎車,光會騎車還不行,他還要認路,哪哪小區在哪裏,哪哪棟在哪裏,從哪走比較近,哪一個門口保安不讓進。這樣就把全部的職能都集中在了老闆身上,作飯與送飯,實際上是兩條事,理論上沒有什麼聯繫,可是這裏若是老闆切菜時,一不當心切到了手,那不光菜作不了,後面也無法送。或者送外賣的路上,爲趕時間闖紅燈被交警攔了下來,不光飯送不了,還回不來作下一份。這就像咱們的代碼全都耦合在一塊兒的後果,兩個業務相互影響,一個業務出現問題另外一個也跟着出現問題,一個業務變動就帶着另外一個業務變動。
咱們想一想,有了外賣小哥以後呢?老闆只要關注於作菜就行了,作好給到外賣小哥。外賣小哥會送到用戶手上。老闆想的是怎麼把菜作的更好吃,外賣小哥想的是怎麼最快送達。職能清晰了,效率就更高了。這裏能夠把老闆當成生產者,對應的外賣小哥就是消費者。
他的第二個好處就是均衡生產者與消費者的能力。
仍是舉外賣的例子。有些外賣是要實時準備的,好比說作菜就是這樣,用戶下單後,老闆馬上洗菜、切菜、作菜而後打包。對於比較耗時的菜品,好比煲粥、燉湯之類的時間可能很長。而外賣小哥耗費的時間只是接到通知後來到這家店的時間。由於如今的外賣系統比較智能,通知的都是距離商戶最近的外賣小哥,因此到店的時間通常比較短。這種場景下瓶頸就是商家的產能,高峯期就可能會形成排隊。以下圖:
java
再嚴重一點就會這樣git
對於這個問題的緣由咱們很清楚了,是由於生產者(商家)的產能跟不上消費者(外賣小哥)的消費(送餐)速度。由於咱們把職能分開了,因此解決問題也很清晰,那就提升生產者的產能,好比說老闆能夠多僱幾個廚師或者再開一家分店。這樣就把生產者的產能提升到與消費者的產能平衡的位置。
還有另外一種生產者比消費者快的狀況,好比說一些小超市,他也有外賣服務。由於他的東西都是現成的,用戶下完單後,只要按訂單裝好就能夠了。這個時候反而是從外邊過來的外賣小哥要慢的多。再或者是商品準備的時間很短,可是送餐的路途遙遠,路況複雜。因此瓶頸到外賣小哥身上。github
這種狀況下問題也很清晰了,消費者消耗的速度跟不上生產者的產能,那擴充消費者的數量好了。好比常常遇到的外賣轉單,一個外賣小哥來不及了,轉給了另外一個外賣小哥。一樣也能達到生產者與消費者的產能均衡。設計模式
好了,說完了 what 還有 why,那麼咱們如今接着說怎麼去實現生產者消費者模式,再也不廢話直接上代碼。
首先咱們寫一個老闆類:多線程
/** * fshows.com * Copyright (C) 2013-2019 All Rights Reserved. */ package cn.coder4j.study.example.thread; import java.util.LinkedList; /** * 老闆 * @author buhao * @version Boss.java, v 0.1 2019-11-09 15:09 buhao */ public class Boss implements Runnable { /** * 最大生產數量 */ public static final int MAX_NUM = 5; /** * 桌子 */ private LinkedList<String> tables; public Boss(LinkedList<String> tables) { this.tables = tables; } @Override public void run() { // 注意點1 while (true){ synchronized (this.tables){ // 注意點2 while (tables.size() == MAX_NUM){ System.out.println("通知外賣小哥取餐"); // 注意點3 this.tables.notifyAll(); try { System.out.println("老闆開始休息了"); this.tables.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } String goods = "牛肉麪" + tables.size(); System.out.println("老闆作了一碗" + goods); tables.addLast(goods); } } } }
而後咱們再寫一個外賣小哥類,可是尷尬的是發現不知道外賣小哥英文怎麼寫,查了一下結果以下
這個 brother 總感受怪怪的,可是我讀書少,他騙我也不知道,就用這個吧。 要是有英語大神能夠留言回覆一下正確怎麼寫。併發
/** * fshows.com * Copyright (C) 2013-2019 All Rights Reserved. */ package cn.coder4j.study.example.thread; import java.util.LinkedList; /** * 外賣小哥 * @author buhao * @version TakeawayBrother.java, v 0.1 2019-11-09 15:14 buhao */ public class TakeawayBrother implements Runnable { private LinkedList<String> tables; public TakeawayBrother(LinkedList<String> tables) { this.tables = tables; } @Override public void run() { while (true){ synchronized (this.tables){ while (this.tables == null || this.tables.size() == 0){ System.out.println("催老闆趕快作外賣"); this.tables.notifyAll(); try { System.out.println("一邊玩手機一邊等外賣"); this.tables.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } String goods = tables.removeFirst(); System.out.println("外賣小哥取餐了" + goods); } } } }
事件發生總歸有一個地方吧,通常老闆把外賣給到外賣小哥都是在店鋪裏,最後咱們再加一個店鋪場景類吧ide
/** * fshows.com * Copyright (C) 2013-2019 All Rights Reserved. */ package cn.coder4j.study.example.thread; import java.util.LinkedList; /** * 店鋪場景 * @author buhao * @version StoreContext.java, v 0.1 2019-11-09 15:28 buhao */ public class StoreContext { public static void main(String[] args) { // 先建立一張用於存放外賣的桌子 LinkedList<String> tables = new LinkedList<>(); // 再建立一個老闆 Boss boss = new Boss(tables); // 最後建立一個外賣小哥 TakeawayBrother takeawayBrother = new TakeawayBrother(tables); // 建立線程對象 Thread bossThread = new Thread(boss); Thread takeawayBrotherThread = new Thread(takeawayBrother); // 運行線程 bossThread.start(); takeawayBrotherThread.start(); } }
老闆作了一碗牛肉麪0 老闆作了一碗牛肉麪1 老闆作了一碗牛肉麪2 老闆作了一碗牛肉麪3 老闆作了一碗牛肉麪4 通知外賣小哥取餐 老闆開始休息了 外賣小哥取餐了牛肉麪0 外賣小哥取餐了牛肉麪1 外賣小哥取餐了牛肉麪2 外賣小哥取餐了牛肉麪3 外賣小哥取餐了牛肉麪4 催老闆趕快作外賣 一邊玩手機一邊等外賣 老闆作了一碗牛肉麪0 老闆作了一碗牛肉麪1 老闆作了一碗牛肉麪2 老闆作了一碗牛肉麪3 老闆作了一碗牛肉麪4 通知外賣小哥取餐 老闆開始休息了 外賣小哥取餐了牛肉麪0 外賣小哥取餐了牛肉麪1 外賣小哥取餐了牛肉麪2 外賣小哥取餐了牛肉麪3 外賣小哥取餐了牛肉麪4 催老闆趕快作外賣 一邊玩手機一邊等外賣 ..........
首先上面的代碼是一個最基本的單生產單消費的例子。若是你想要多生產多消費,那多建立幾個 boss 或者 takeawayBrother 就能夠了。
而後店鋪場景類沒什麼可說的,只是基本的建立線程邏輯,若是對於線程建立不瞭解的,能夠參考前文的【併發那些事】建立線程的三種方式。此文再也不贅述。另外觀察代碼,能夠發現生產者與消費者的代碼極爲類似,只是一個存一個取。這裏咱們以生產者爲例子說明。
首先在 Boss 類中他有兩個成員屬性,一個是 MAX_NUM 一個是 tables。還記得咱們在一開頭提到的『固定大小的緩衝區』嗎?這裏的 MAX_NUM 對應的就是『固定大小』這幾個字,這裏咱們設置的是 5 個。他的現實意義就是老闆不可能從早到晚一刻不停的作菜,通常是在點單的時候開始作,也有一些在高峯期的時候提早作一點,可是他放菜的桌子只有那麼大,放滿了就不能接着作。而 tables 就對應着『緩衝區』這幾個字。老闆作完菜總要有一個地方先放着等外賣小哥來拿吧,緩衝區就是放菜的桌子。
而後咱們再接着看代碼邏輯,我在代碼中標記了幾個注意點。
第一個注意點是最外面一層的 while。這個是多線程通用寫法,由於不寫 while 的話,一次任務結束後代碼就退出了。現實業務中咱們一般想要業務一直持續的運行,因此加個 while 解決。
第二個注意點 while (tables.size() == MAX_NUM) 。這個信息量相對多一點,首先 while 的判斷條件的意思是判斷當前桌子上的外賣是否是已經達到上限,若是是會進入 while 代碼塊的內容,首先通知(notifyAll)外賣小哥能夠拿外賣了,而後本身能夠歇着了(wait),不然接着往下走繼續作。初次接觸生產消費模型的同窗,很容易出錯的點就是把這裏的 while 寫成 if。由於這裏自己也只是要判斷當前緩衝區是否知足生產的條件。其實在語法與邏輯上沒有問題,可是在多線程下就會出現 虛假喚醒 的問題。好比如今有兩個生產者都處於調用 wait 的地方。忽然消費者線程把數據消費完了,並通知了全部生產者去生產,兩個生產者都接收到消息,可是隻有一個生產者拿到鎖,他就去生產了,生產完後,把鎖就釋放了,剛剛另外一個接收到消息的生產者拿到鎖就接着往下走,若是這裏是 if 的話,由於都已經判斷過了,不會再判斷,可是明顯另外一個線程已經完了任務,他如今已經不符合條件。接着往下走就會出現問題。因此當這裏換成 while 後,他醒來後還會接着判斷一次,不知足就接着等待,這樣就避免了虛假喚醒這種問題。
第三個注意點 this.tables.notifyAll()。關於第二個問題,你們可能要說了,出現問題是由於咱們同時通知了兩個生產者形成的,java 自帶了一個喚醒單個線程的 notify 方法爲何不用,反而用喚醒全部線程的 notifyAll 方法。這是由於 notify 喚醒線程是 隨機 的,也就是說你喚醒的多是生產者也多是消費者。好比說你是生產者,你生產夠了,你想喚醒消費者,可是不幸的是你喚醒了另外一個生產者,另外一個生產者一覺醒來,發現菜都作完了,就接着睡,若是生產者一直喚醒的都是生產者,那麼程序就會進入 假死 狀態,消費者永遠都處於等待狀態。post
由於篇幅有限,沒法貼完全部代碼,如遇到問題可到github上查看源碼。this