全面理解線程間的通信方式


因為存在對共享變量的操作,才有了討論線程的話題。在線程中存在這樣一種場景,一個線程操作了共享變量的值而另一個線程感知了此次操作,然后進行相應的操作。整個過程開始於一個線程,結束與另一個線程。前者我們可以稱作生產者,后者我們可以稱作消費者,因為有了前者的活動才出發后者邏輯的發生,這種隔離模式在功能實現方面具備了良好的伸縮性。等待/通知的java方法是任何對象都具備的,因為這些方法被定義在java的超類java.lang.Object上下圖展示了Object 類的所有方法:

等待、通知機制就是,線程A調用了對象Owait()方法進入等待狀態,線程B 調用了對象Onotify()或者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()所要注意的點:

  1. 調用 wait()/notify()/notifyAll() 方法的線程必須是持有該對象的鎖的線程
  2. 調用wait()方法后線程由RUNNING狀態變為WAITING狀態,並將當前線程放置在該對象的等待隊列中,同時釋放擁有的鎖
  3. 調用notify()notifyAll()之后線程並不會立即從wait()方法處返回,而是需要等待調用notify()/notifyAll()的線程釋放鎖之后才會返回。
  4. 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()都是鎖級別的操作,操作這幾個方法的線程必須持有該對象的鎖,而鎖又是屬於對象的。每一個對象的對象頭中有幾位是標識鎖的狀態的,所以實際上鎖是屬於對象的並不是屬於線程的。如果這幾個方法定義在線程中會造成極大的不便,在實際的操作中我們會遇到一個線程獲取幾把鎖的情況,如果將鎖定義在線程中時間這種情況就不是那么的方便了。

3wait()方法是屬於對象的,那調用Thread.wait()會怎樣?

調用Thread.wait() 也就是說將Thread 當做鎖對象,持有Thread對象的鎖的線程在執行結束后會自動調用notify(),所以我們應該避免使用線程對象來作為鎖對象。

4notifyAll() 會喚醒所有的線程同時去爭奪這把鎖,如果沒有獲取到鎖的對象該怎么辦?

沒有搶到鎖的線程會再次進入WAITING狀態,進入對象的等待隊列中去,直至有其他線程再次調用notify()或者notify()All 或者調用該線程的中斷方法。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM