1. 程式人生 > >(2.1.27.7)Java併發程式設計:Object.wait/notify

(2.1.27.7)Java併發程式設計:Object.wait/notify

Java Object物件中的wait,notify,notifyAll是定義在Object類的例項方法,用於控制執行緒狀態

三個方法都必須在synchronized 同步關鍵字所限定的作用域中呼叫,否則會報錯java.lang.IllegalMonitorStateException ,意思是因為沒有同步,所以執行緒對物件鎖的狀態是不確定的,不能呼叫這些方法。

  • wait
    • 表示持有物件鎖的執行緒A準備釋放物件鎖許可權,釋放cpu資源並進入等待。
  • notify
    • 表示持有物件鎖的執行緒A準備釋放物件鎖許可權,通知jvm喚醒某個隨機競爭該物件鎖的執行緒X。
    • 執行緒A synchronized 程式碼作用域結束後,執行緒X直接獲得物件鎖許可權,其他競爭執行緒繼續等待(即使執行緒X同步完畢,釋放物件鎖,其他競爭執行緒仍然等待,直至有新的notify ,notifyAll被呼叫)。
  • notifyAll
    • 表示持有物件鎖的執行緒A準備釋放物件鎖許可權,通知jvm喚醒所有競爭該物件鎖的執行緒
    • 執行緒A synchronized 程式碼作用域結束後,jvm通過演算法將物件鎖許可權指派給某個執行緒X,所有被喚醒的執行緒不再等待。執行緒X synchronized 程式碼作用域結束後,之前所有被喚醒的執行緒都有可能獲得該物件鎖許可權,這個由JVM演算法決定

我們不禁產生這樣的疑問:

  1. 進入wait/notify方法之前,為什麼要獲取synchronized鎖?
  2. 執行緒A獲取了synchronized鎖,執行wait方法並掛起,執行緒B又如何再次獲取鎖?

一、示例

public class WaitNotifyCase {
    public static void main(String[] args) {
        final Object lock = new Object();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread A is waiting to get lock");
                synchronized (lock) {
                    try {
                        System.out.println("thread A get lock");
                        TimeUnit.SECONDS.sleep(1);
                        System.out.println("thread A do wait method");
                        lock.wait();
                        System.out.println("wait end");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread B is waiting to get lock");
                synchronized (lock) {
                    System.out.println("thread B get lock");
                    try {
                        TimeUnit.SECONDS.sleep(5);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    lock.notify();
                    System.out.println("thread B do notify method");
                }
            }
        }).start();
    }
}


執行結果:

thread A is waiting to get lock
thread A get lock
thread B is waiting to get lock
thread A do wait method
thread B get lock
thread B do notify method
wait end

由同一個lock物件呼叫wait、notify方法。 1、當執行緒A執行wait方法時,該執行緒會被掛起; 2、當執行緒B執行notify方法時,會喚醒一個被掛起的執行緒A;

lock物件、執行緒A和執行緒B三者是一種什麼關係?根據上面的結論,可以想象一個場景: 1、lock物件維護了一個等待佇列list; 2、執行緒A中執行lock的wait方法,把執行緒A儲存到list中; 3、執行緒B中執行lock的notify方法,從等待佇列中取出執行緒A繼續執行;

二、為什麼要使用synchronized?

我們知道wait/notify是為了執行緒間協作而設計的,當我們執行wait的時候讓執行緒掛起,當執行notify的時候喚醒其中一個掛起的執行緒,那需要有個地方來儲存物件和執行緒之間的對映關係(可以想象一個map,key是物件,value是一個執行緒列表)

當呼叫這個物件的wait方法時,將當前執行緒放到這個執行緒列表裡,當呼叫這個物件的notify方法時從這個執行緒列表裡取出一個來讓其繼續執行

這樣看來是可行的,也比較簡單,那現在的問題這種對映關係放到哪裡?

lock.wait()方法通過呼叫native方法wait(0)實現,其中介面註釋中有這麼一句:

The current thread must own this object’s monitor.

表示執行緒執行lock.wait()方法時,必須持有該lock物件的ObjectMonitor(前文已經提及,每個鎖物件(這裡指已經升級為重量級鎖的物件)都有一個ObjectMonitor(物件監視器)。也就是說每個執行緒獲取鎖物件都會通過ObjectMonitor)

如果wait方法在synchronized程式碼中執行,該執行緒很顯然已經持有了monitor。

從而包含了“ 進入wait/notify方法之前,為什麼要獲取synchronized鎖”,synchronized程式碼塊通過javap生成的位元組碼中包含 *monitorentermonitorexit指令。其中執行monitorenter指令可以獲取物件的monitor

在這裡插入圖片描述 【為什麼要使用synchronized】

三、程式碼執行過程分析

  1. 在多核環境下,執行緒A和B有可能同時執行monitorenter指令,並獲取lock物件關聯的monitor,只有一個執行緒可以和monitor建立關聯,假設執行緒A執行加鎖成功;
  2. 執行緒B競爭加鎖失敗,進入等待佇列進行等待;
  3. 執行緒A繼續執行,當執行到wait方法

3.1 Object.wait方法實現

wait方法會將當前執行緒放入wait set,等待被喚醒,並放棄lock物件上的所有同步宣告,意味著"執行緒A釋放了鎖,執行緒B可以重新執行加鎖操作"

Object.wait()在使用時通常要判斷是否滿足某個條件,不滿足某個外部條件cond時呼叫wait(),來讓執行緒阻塞同時釋放被synchronized鎖定的mutex;從這個過程看來Object.wait()實際上是起到條件變數的作用

  1. wait()內部實際上先將synchronized鎖定的鎖釋放
  2. 之後將當前執行緒阻塞在某個內建的條件condition上(注意:此condition為內建的,與外部判斷的條件cond並非同一個,外部的cond需要程式設計師根據程式邏輯來判斷改變,而這個condition只能被Object.notify()/notifyAll()改變),直到內建條件condition被Object.notify()/notifyAll()修改時才會重新鎖定該mutex,繼續執行wait()後的程式碼。
wait() {
    unlock(mutex);//解鎖mutex
    wait_condition(condition);//等待內建條件變數condition
    lock(mutex);//競爭鎖
}

lock.wait()方法最終通過ObjectMonitor的void wait(jlong millis, bool interruptable, TRAPS);實現:

1、將當前執行緒封裝成ObjectWaiter物件node; 在這裡插入圖片描述 【wait方法實現1】

2、通過ObjectMonitor::AddWaiter方法將node新增到_WaitSet列表中;

在這裡插入圖片描述 【wait方法實現2】

3、通過ObjectMonitor::exit方法釋放當前的ObjectMonitor物件,這樣其它競爭執行緒就可以獲取該ObjectMonitor物件。

在這裡插入圖片描述 【wait方法實現3】

4、最終底層的park方法會掛起執行緒; 5、ObjectMonitor中的其他競爭執行緒被CPU自動喚醒和選擇某個啟動執行

3.2 Object.notify()/notifyAll方法實現

Object.notify()/notifyAll()實際上只起到一個sinal內建條件變數的作用,呼叫Object.notify()/notifyAll()之後,這個時候其他處於wait()中的執行緒所等待的內建條件變數已經滿足,但是由於wait()中仍然需要lock mutex

而在Object.notify()/notifyAll()中沒有把mutex釋放掉,故阻塞在wait()處的執行緒繼續等待,但等待的條件不再是內建條件變數而是鎖mutex;直到synchronized程式碼塊結束時,由於會自動釋放被synchronized鎖定的mutex,故此時所有在wait()中等待mutex的執行緒開始競爭mutex,得到該mutex的會繼續執行,否則繼續等待mutex

obj.notify()/notifyAll(){
    condition=true;//只起到把內建條件變數置為true的作用
}
  • lock.notify()方法最終通過ObjectMonitor的void notify(TRAPS)實現:

    1. 如果當前_WaitSet為空,即沒有正在等待的執行緒,則直接返回;
    2. 通過ObjectMonitor::DequeueWaiter方法,獲取_WaitSet列表中的第一個ObjectWaiter節點,實現也很簡單。這裡需要注意的是,在jdk的notify方法註釋是隨機喚醒一個執行緒,其實是第一個ObjectWaiter節點
    3. 根據不同的策略,將取出來的ObjectWaiter節點,加入到_EntryList或則通過Atomic::cmpxchg_ptr指令進行自旋操作cxq,具體程式碼實現有點長,這裡就不貼了,有興趣的同學可以看objectMonitor::notify方法;

    在這裡插入圖片描述 【Object.notify()notifyAll方法實現】

  • lock.notifyAll()方法最終通過ObjectMonitor的void notifyAll(TRAPS)實現:

    1. 通過for迴圈取出_WaitSet的ObjectWaiter節點,並根據不同策略,加入到_EntryList或則進行自旋操作。

從JVM的方法實現中,可以發現:notify和notifyAll並不會釋放所佔有的ObjectMonitor物件. 其實真正釋放ObjectMonitor物件的時間點是在執行monitorexit指令,一旦釋放ObjectMonitor物件了,entry set中ObjectWaiter節點所儲存的執行緒就可以開始競爭ObjectMonitor物件進行加鎖操作了。

  • 被notify(All)的執行緒有規律嗎
    • 如果是通過notify來喚起的執行緒,那先進入wait的執行緒會先被喚起來
    • 如果是通過nootifyAll喚起的執行緒,預設情況是最後進入的會先被喚起來,即LIFO的策略