1. 程式人生 > >阿里巴巴面試題: 為什麼wait()和notify()需要搭配synchonized關鍵字使用

阿里巴巴面試題: 為什麼wait()和notify()需要搭配synchonized關鍵字使用

本文主要參考

理解此問題先修知識:

  • synchronized 的含義:

    • Java中每一個物件都可以成為一個監視器(Monitor), 該Monitor由一個鎖(lock), 一個等待佇列(waiting queue ), 一個入口佇列( entry queue).
    • 對於一個物件的方法, 如果沒有synchronized關鍵字, 該方法可以被任意數量的執行緒,在任意時刻呼叫。
    • 對於添加了synchronized關鍵字的方法,任意時刻只能被唯一的一個獲得了物件例項鎖的執行緒呼叫。
    • synchronized用於實現多執行緒的同步操作
  • wait()功用

    • wait(), notify()
      , notifyAll()synchonized 需要搭配使用, 用於執行緒同步
    • wait()總是在一個迴圈中被呼叫,掛起當前執行緒來等待一個條件的成立。 Wait呼叫會一直等到其他執行緒呼叫notifyAll()時才返回。
    • 當一個執行緒在執行synchronized 的方法內部,呼叫了wait()後, 該執行緒會釋放該物件的鎖, 然後該執行緒會被新增到該物件的等待佇列中(waiting queue), 只要該執行緒在等待佇列中, 就會一直處於閒置狀態, 不會被排程執行。 要注意wait()方法會強迫執行緒先進行釋放鎖操作,所以在呼叫wait()時, 該執行緒必須已經獲得鎖,否則會丟擲異常。由於wait()
      在synchonized的方法內部被執行, 鎖一定已經獲得, 就不會丟擲異常了。
      這裡寫圖片描述
  • notify()的功用

    • wait(), notify(), notifyAll()synchonized 需要搭配使用, 用於執行緒同步
    • 當一個執行緒呼叫一個物件的notify()方法時, 排程器會從所有處於該物件等待佇列(waiting queue)的執行緒中取出任意一個執行緒, 將其新增到入口佇列( entry queue) 中. 然後在入口佇列中的多個執行緒就會競爭物件的鎖, 得到鎖的執行緒就可以繼續執行。 如果等待佇列中(waiting queue)沒有執行緒, notify()方法不會產生任何作用
    • notifyAll()notify()工作機制一樣, 區別在於notifyAll()會將等待佇列(waiting queue)中所有的執行緒都新增到入口佇列中(entry queue)
    • 注意, notifyAll()notify()更加常用, 因為notify()方法只會喚起一個執行緒, 且無法指定喚醒哪一個執行緒,所以只有在多個執行相同任務的執行緒在併發執行時, 我們不關心哪一個執行緒被喚醒時,才會使用notify()

為什麼wait()notify()notifyAll()需要搭配synchronized關鍵字使用

  • 從語義角度來講, 一個執行緒呼叫了wait()之後, 必然需要由另外一個執行緒呼叫notify()來喚醒該執行緒, 所以本質上, wait()notify()的成對使用, 是一種執行緒間的通訊手段。
  • 進一步分析, wait() 操作的呼叫必然是在等待某種條件的成立, 而條件的成立必然是由其他的執行緒來完成的。 所以實際上, 我們呼叫 wait() 的時候, 實際上希望達到如下的效果
// 執行緒A 的程式碼
while(!condition){ // 不能使用 if , 因為存在一些特殊情況, 使得執行緒沒有收到 notify 時也能退出等待狀態
    wait();
}
// do something
// 執行緒 B 的程式碼
if(!condition){ 
    // do something ...
    condition = true;
    notify();
}
  • 現在考慮, 如果wait()notify() 的操作沒有相應的同步機制, 則會發生如下情況

    1. 【執行緒A】 進入了 while 迴圈後突然被掛起
    2. 【執行緒B】 執行完畢了 condition = true; notify(); 的操作, 此時【執行緒A】的 wait() 操作尚未被執行, notify() 操作沒有產生任何效果
    3. 【執行緒A】執行wait() 操作, 進入等待狀態,如果沒有額外的 notify() 操作, 該執行緒將持續在 condition = true 的情形下, 持續處於等待狀態得不到執行。
  • 那是否簡單的將之前的程式碼包裹在一個 synchronized 程式碼塊中就可以滿足需求呢? 像下面這樣。

// 執行緒A 的程式碼
synchronized(obj_A)
{
    while(!condition){ 
        wait();
    }
    // do something 
}
// 執行緒 B 的程式碼
synchronized(obj_A)
{
    if(!condition){ 
        // do something ...
        condition = true;
        notify();
    }
}
  • 乍一看, 上述的程式碼可以解決問題, 但是仔細分析一下, 由於wait() 操作會掛起當前執行緒, 那麼必然需要在掛起前釋放掉 obj_A 的鎖, 但如果 obj_A 允許是任意一個指定的物件, wait() 操作內部必然無法得知應該釋放哪個物件的鎖, 除非將需要釋放鎖的物件作為引數傳給 wait() 方法。那麼很自然的, 語法就會被設計成 java 現在的樣子。即基於物件的 wait()notify() 的呼叫, 必須先獲得該物件的鎖。

  • 正確的用法示例如下

// 執行緒 A 的程式碼
synchronized(obj_A)
{
    while(!condition){ 
        obj_A.wait();
    }
    // do something 
}
// 執行緒 B 的程式碼
synchronized(obj_A)
{
    if(!condition){ 
        // do something ...
        condition = true;
        obj_A.notify();
    }
}