Java築基之執行緒併發生產消費者模式
技術標籤:Java 築基筆記集java多執行緒
執行緒併發協作(生產者/消費者模式)
負責生產資料的模組(這裡模組可能是:方法、物件、執行緒、程序)。
負責處理資料的模組(這裡模組可能是:方法、物件、執行緒、程序)。
消費者不能直接使用生產者的資料,它們之間有個“倉庫”。生產者將生產好的資料放入“倉庫”,消費者從“倉庫”拿要處理的資料
- 注意:
- 倉庫作為消費者與生產者之間的緩衝區,使生產與消費的執行緒分離大道解耦的效果
- 解決生產消費過載,生產資料慢時,緩衝區仍有資料,不影響消費者消費;消費處理資料慢時,生產者仍然可以繼續往緩衝區裡面放置資料
例項需求
- 《wait/notify實現生產者和消費者程式》
- 採用多執行緒技術,例如wait/notify,設計實現一個符合生產者和消費者問題的程式,
- 對某一個物件(槍膛)進行操作,其最大容量是10顆子彈,
- 生產者執行緒是一個壓入執行緒,它不斷向槍膛中壓入子彈,
- 消費者執行緒是一個射出執行緒,它不斷從槍膛中射出子彈。
/**
* @author LHW
* @date 2020/12/25.
* Description:子彈實體類
*/
public class Bullet {
public int id;
}
/** * @author LHW * @date 2020/12/25. * Description:槍膛 壓入發射子彈 */ public class Clip { public int index = 0;//壓入與發射的標誌位 public Bullet[] bullets = new Bullet[10];//快取子彈彈夾 //壓入子彈入槍膛 public synchronized void pushBullet(Bullet bullet) { try { while (index == bullets.length) { System.out.println("pushBullet 彈夾已滿進入等待。。。。。index=" + index); this.wait(); } } catch (InterruptedException e) { e.printStackTrace(); } this.notify();//喚醒等待執行緒 bullets[index] = bullet; index++; } //槍膛射出子彈 public synchronized Bullet shootBullet() { try { while (index == 0) { System.out.println("shootBullet 槍膛為空進入等待。。。。。index=" + index); wait(); } } catch (InterruptedException e) { e.printStackTrace(); } this.notify(); index--; return bullets[index]; } }
/** * @author LHW * @date 2020/12/25. * Description: * 《wait/notify實現生產者和消費者程式》 * 採用多執行緒技術,例如wait/notify,設計實現一個符合生產者和消費者問題的程式, * 對某一個物件(槍膛)進行操作,其最大容量是10顆子彈, * 生產者執行緒是一個壓入執行緒,它不斷向槍膛中壓入子彈, * 消費者執行緒是一個射出執行緒,它不斷從槍膛中射出子彈。 */ public class ShootingMain { public static void main(String[] args) { Clip mClip = new Clip(); new PushThread(mClip).start(); new ShootThread(mClip).start(); } static class PushThread extends Thread { Clip mClip; PushThread(Clip clip) { mClip = clip; } @Override public void run() { super.run(); for (int i = 0; i < mClip.bullets.length; i++) { Bullet bullet = new Bullet(); bullet.id = i; mClip.pushBullet(bullet); System.out.println("PushThread 子彈壓入槍膛。。。" + bullet.id); //如果這裡不執行睡眠等待 生產會壓入所有子彈後執行消費執行緒的射出子彈 SleepTools.ms(1000);//睡眠一秒 阻塞生產執行緒 執行消費執行緒 } } } static class ShootThread extends Thread { Clip mClip; ShootThread(Clip clip) { mClip = clip; } @Override public void run() { super.run(); for (int i = 0; i < mClip.bullets.length; i++) { Bullet bullet = mClip.shootBullet(); System.out.println("ShootThread 子彈射出槍膛。。。" + bullet.id); SleepTools.ms(1000);//睡眠一秒 阻塞消費執行緒 執行生產執行緒 } } } }
結果輸出
PushThread 子彈壓入槍膛。。。0
ShootThread 子彈射出槍膛。。。0
shootBullet 槍膛為空進入等待。。。。。index=0
PushThread 子彈壓入槍膛。。。1
ShootThread 子彈射出槍膛。。。1
shootBullet 槍膛為空進入等待。。。。。index=0
PushThread 子彈壓入槍膛。。。2
ShootThread 子彈射出槍膛。。。2
PushThread 子彈壓入槍膛。。。3
ShootThread 子彈射出槍膛。。。3
PushThread 子彈壓入槍膛。。。4
ShootThread 子彈射出槍膛。。。4
shootBullet 槍膛為空進入等待。。。。。index=0
PushThread 子彈壓入槍膛。。。5
ShootThread 子彈射出槍膛。。。5
PushThread 子彈壓入槍膛。。。6
ShootThread 子彈射出槍膛。。。6
shootBullet 槍膛為空進入等待。。。。。index=0
PushThread 子彈壓入槍膛。。。7
ShootThread 子彈射出槍膛。。。7
shootBullet 槍膛為空進入等待。。。。。index=0
PushThread 子彈壓入槍膛。。。8
ShootThread 子彈射出槍膛。。。8
PushThread 子彈壓入槍膛。。。9
ShootThread 子彈射出槍膛。。。9
總結
消費者/生產模式通常用於如下情景:
- 1.生產與消費共享同一個資源,並且生產者和消費者之間相互依賴,互為條件
- 2.生產者沒有生產產品前即資源為空時,消費者進入等待,否則通知消費者進行消費
- 3.消費者消費後要通知生產者已消費完,需要通知生產者進行生產產品以供消費
執行緒直接的協作
- 協作(等待 / 通知機制)情景
是指一個執行緒 A 呼叫了物件 User 的 wait()方法進入等待狀態,而另一個執行緒 B
呼叫了物件User 的 notify()或者 notifyAll()方法,執行緒A 收到通知後從物件User 的wait()方法返回,進而執行後續操作。上述兩個執行緒通過物件User來完成互動,而物件上的 wait()和 notify/notifyAll()的關係就如同開關訊號一樣,用來完成等待方和通知方之間的互動工作。
- notify():
- 通知一個在物件上等待的執行緒,使其從wait方法返回,而返回的前提是該執行緒
獲取到了物件的鎖,沒有獲得鎖的執行緒重新進入 WAITING 狀態。
- notifyAll():
通知所有等待在該物件上的執行緒
- wait()
呼叫該方法的執行緒進入 WAITING 狀態,只有等待另外執行緒的通知或被中斷
才會返回.需要注意,呼叫 wait()方法後,會釋放物件的鎖
- wait(long)
超時等待一段時間,這裡的引數時間是毫秒,也就是等待長達n毫秒,如果沒有
通知就超時返回
- wait (long,int)
-對於超時時間更細粒度的控制,可以達到納秒
注意:
以上方法均是java.lang.Object類的方法;
都只能在同步方法或者同步程式碼塊中使用,否則會丟擲異常。
等待和通知的標準正規化
等待方遵循如下原則
- 獲取物件的鎖
- 如果條件不滿足,那麼呼叫的物件的wait()方法,被通知後仍要檢查條件
- 條件滿足則執行對應的邏輯
synchronized(物件){
while(條件不滿足){
物件.wait();
}
對應的處理邏輯
}
通知方遵循如下原則
- 獲取物件的鎖
- 改變條件
- 通知所有等待在物件上的執行緒
synchronized(物件){
改變條件
物件.notifyAll();
}
在呼叫 wait () 、notify() 系列方法 之前,執行緒必須要獲得該物件的物件級
別鎖,即只能在同步方法或同步塊中呼叫 wait ()方法 、notify() 系列方法,進
入 wait()方法後,當前執行緒釋放鎖,在從 wait()返回前,執行緒與其他執行緒競
爭重新獲得鎖,執行 notify()系列方法的執行緒退出呼叫了 notifyAll 的 synchronized
程式碼塊的時候後,他們就會去競爭。如果其中一個執行緒獲得了該物件鎖,它就會
繼續往下執行,在它退出 synchronized 程式碼塊,釋放鎖後,其他的已經被喚醒的
執行緒將會繼續競爭獲取該鎖,一直進行下去,直到所有被喚醒的執行緒都執行完畢
注意事項:
1.notify和notifyAll應該用誰?
- 儘可能用 notifyall(),謹慎使用 notify(),因為 notify()只會喚醒一個執行緒,我
們無法確保被喚醒的這個執行緒一定就是我們需要喚醒的執行緒
2.呼叫 yield() 、sleep()、wait()、notify()等方法對鎖有何影響?
- yield() 、sleep()被呼叫後,都不會釋放當前執行緒所持有的鎖。
- 呼叫 wait()方法後,會釋放當前執行緒持有的鎖,而且當前被喚醒後,會重新
去競爭鎖,鎖競爭到後才會執行 wait 方法後面的程式碼。 - 呼叫 notify()系列方法後,對鎖無影響,執行緒只有在 syn 同步程式碼執行完後才
會自然而然的釋放鎖,所以 notify()系列方法一般都是 syn 同步程式碼的最後一行