全面理解執行緒間的通訊方式
- 2020 年 5 月 8 日
- 筆記
- JAVA, notify() notifyAll, wait, 多執行緒
因為存在對共享變數的操作,才有了討論執行緒的話題。在執行緒中存在這樣一種場景,一個執行緒操作了共享變數的值而另一個執行緒感知了此次操作,然後進行相應的操作。整個過程開始於一個執行緒,結束與另一個執行緒。前者我們可以稱作生產者,後者我們可以稱作消費者,因為有了前者的活動才出發後者邏輯的發生,這種隔離模式在功能實現方面具備了良好的伸縮性。等待/通知的java方法是任何對象都具備的,因為這些方法被定義在java的超類java.lang.Object上下圖展示了Object 類的所有方法:
等待、通知機制就是,執行緒A調用了對象O的wait()方法進入等待狀態,執行緒B 調用了對象O的notify()或者notifyAll() 喚醒處於對象O上等待的執行緒A,使執行緒A 從wait()方法處返回從而進行剩餘操作。上述兩個執行緒通過對象O來進行交互,而對象O上的wait()/notify()/notifyAll()就如同訊號一般控制著執行緒的操作。這種例子生活中隨處可見,例如商品入庫,如果倉庫中是滿的我們就無法將商品再放入倉庫,如果倉庫中沒有商品我們也無法從倉庫中取出商品,再舉一個日常發生在我們身邊的場景。現在網購越來越方便,我們與快遞之間有一個快遞小哥在關聯,快遞小哥將包裹放到快遞櫃,我們去快遞櫃領取快遞:
1 /** 2 * 快遞 3 */ 4 public class Courier extends Thread { 5 6 @SneakyThrows 7 @Override 8 public void run() { 9 10 synchronized (CourierCabinet.CABINET) { 11 while (true) { 12 if (CourierCabinet.CABINET.size() == 10) { 13 //歇一歇吧 快遞櫃沒地方了 14 CourierCabinet.CABINET.wait(); 15 } 16 CourierCabinet.CABINET.add("包裹"); 17 System.out.println("親愛的顧客您的快遞已入櫃,請及時來領取"); 18 CourierCabinet.CABINET.notify(); 19 Thread.sleep(100); 20 21 } 22 } 23 24 } 25 }
1 /** 2 * 收件人 3 */ 4 public class Recipient extends Thread { 5 6 7 @SneakyThrows 8 @Override 9 public void run() { 10 11 while (true) { 12 synchronized (CourierCabinet.CABINET) { 13 14 if (CourierCabinet.CABINET.size() == 0) { 15 // 快遞員還未將包裹入櫃 沒法領取等一等 16 CourierCabinet.CABINET.wait(); 17 } 18 19 CourierCabinet.CABINET.remove("包裹"); 20 System.out.println("哈哈 領到了我的快遞..."); 21 CourierCabinet.CABINET.notify(); 22 Thread.sleep(100); 23 } 24 } 25 } 26 }
1 ** 2 * 快遞櫃 3 */ 4 public class CourierCabinet { 5 6 /** 7 * 快遞櫃容量 8 */ 9 public static final List<String> CABINET = new ArrayList<>(10); 10 11 12 public static void main(String[] args) { 13 14 15 new Thread(new Courier()).start(); 16 17 18 new Thread(new Recipient()).start(); 19 20 21 } 22 23 }
運行結果:
通過以上例子,可以總結出 使用對象的wait()/notify()/notifyAll()所要注意的點:
- 調用 wait()/notify()/notifyAll() 方法的執行緒必須是持有該對象的鎖的執行緒
- 調用wait()方法後執行緒由RUNNING狀態變為WAITING狀態,並將當前執行緒放置在該對象的等待隊列中,同時釋放擁有的鎖
- 調用notify()和notifyAll()之後執行緒並不會立即從wait()方法處返回,而是需要等待調用notify()/notifyAll()的執行緒釋放鎖之後才會返回。
- notify() 將等待隊列中的執行緒移動到同步隊列中去,notifyAll()將等待隊列中所有的執行緒移動到同步隊列中去,此時被移動的執行緒狀態由WAITING 轉為BLOCKED
關於執行緒同步、通知機制面試題
1:為什麼操作 wait() notify() notifyAll() 需要事先獲取鎖,
主要是為了防止死鎖了永久等待的發生,以上面的例子說明,收件人執行緒執行if(CABINET.size()==0)的時候滿足條件 由於沒有synchronize 加持,所以該執行緒並不一定會執行CABINET.wait() 可能被CPU切走了,執行緒進入了BLOCKED狀態。
此時快遞小哥執行緒獲取到了執行權,判斷if(CABINET.size==10)不滿足條件,然後執行CABINET.add(包裹操作)執行notify()因為收件人執行緒並沒有執行wait(),所以就可能處於一直等待中。就如同你給我打電話 我還沒有拿到電話你就已經打過了 此時我再拿到電話也不會收到你的電話了。
Wait()放到synchronize 中執行就是為了保證執行緒安全,如果一個執行緒想要從wait()處返回也需要獲取到該對象的鎖否則會出現IllegalMonitorStateException異常。
2:為什麼執行緒通訊的方法wait()notify()notifyAll()是定義在Object中而sleep()定義在執行緒類中?
主要因為java中的wait()notify()notifyAll()都是鎖級別的操作,操作這幾個方法的執行緒必須持有該對象的鎖,而鎖又是屬於對象的。每一個對象的對象頭中有幾位是標識鎖的狀態的,所以實際上鎖是屬於對象的並不是屬於執行緒的。如果這幾個方法定義在執行緒中會造成極大的不便,在實際的操作中我們會遇到一個執行緒獲取幾把鎖的情況,如果將鎖定義在執行緒中時間這種情況就不是那麼的方便了。
3:wait()方法是屬於對象的,那調用Thread.wait()會怎樣?
調用Thread.wait() 也就是說將Thread 當做鎖對象,持有Thread對象的鎖的執行緒在執行結束後會自動調用notify(),所以我們應該避免使用執行緒對象來作為鎖對象。
4:notifyAll() 會喚醒所有的執行緒同時去爭奪這把鎖,如果沒有獲取到鎖的對象該怎麼辦?
沒有搶到鎖的執行緒會再次進入WAITING狀態,進入對象的等待隊列中去,直至有其他執行緒再次調用notify()或者notify()All 或者調用該執行緒的中斷方法。