JVM原始碼分析之Object.wait/notify實現

架構師springboot發表於2019-04-04

最簡單的東西,往往包含了最複雜的實現,因為需要為上層的存在提供一個穩定的基礎,Object作為java中所有物件的基類,其存在的價值不言而喻,其中wait和notify方法的實現多執行緒協作提供了保證。

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繼續執行;
當然了,Hotspot實現不可能這麼簡單。

上述程式碼中,存在多個疑問:

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

為什麼要使用synchronized?
static void Sort(int [] array) {
    // synchronize this operation so that some other thread can't
    // manipulate the array while we are sorting it. This assumes that other
    // threads also synchronize their accesses to the array.
    synchronized(array) {
        // now sort elements in array
    }
}
複製程式碼

synchronized程式碼塊通過javap生成的位元組碼中包含 ** monitorenter ** 和 ** monitorexit **指令。


JVM原始碼分析之Object.wait/notify實現

執行monitorenter指令可以獲取物件的monitor,而lock.wait()方法通過呼叫native方法wait(0)實現,其中介面註釋中有這麼一句:

The current thread must own this object's monitor.

表示執行緒執行lock.wait()方法時,必須持有該lock物件的monitor,如果wait方法在synchronized程式碼中執行,該執行緒很顯然已經持有了monitor。

程式碼執行過程分析

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

This method causes the current thread to place itself in the wait set for this object and then to relinquish any and all synchronization claims on this object.

wait方法會將當前執行緒放入wait set,等待被喚醒,並放棄lock物件上的所有同步宣告,意味著執行緒A釋放了鎖,執行緒B可以重新執行加鎖操作,不過又有一個疑問:線上程A的wait方法釋放鎖,到執行緒B獲取鎖,這期間發生了什麼?執行緒B是如何知道執行緒A已經釋放了鎖?好迷茫....

4、執行緒B執行加鎖操作成功,對於notify方法,JDK註釋:notify方法會選擇wait set中任意一個執行緒進行喚醒;

Wakes up a single thread that is waiting on this object's monitor. If any threads are waiting on this object, one of them is chosen to be awakened. The choice is arbitrary and occurs at the discretion of the implementation

notifyAll方法的註釋:notifyAll方法會喚醒monitor的wait set中所有執行緒。

Wakes up all threads that are waiting on this object's monitor.

5、執行完notify方法,並不會立馬喚醒等待執行緒,在notify方法後面加一段sleep程式碼就可以看到效果,如果執行緒B執行完notify方法之後sleep 5s,在這段時間內,執行緒B依舊持有monitor,執行緒A只能繼續等待;

那麼wait set的執行緒什麼時候會被喚醒?

想要解答這些疑問, 需要分析jvm的相關實現,本文以HotSpot虛擬機器1.7版本為例

什麼是monitor?

在HotSpot虛擬機器中,monitor採用ObjectMonitor實現。


JVM原始碼分析之Object.wait/notify實現

每個執行緒都有兩個ObjectMonitor物件列表,分別為free和used列表,如果當前free列表為空,執行緒將向全域性global list請求分配ObjectMonitor。

ObjectMonitor物件中有兩個佇列:_WaitSet 和 _EntryList,用來儲存ObjectWaiter物件列表;_owner指向獲得ObjectMonitor物件的執行緒。

JVM原始碼分析之Object.wait/notify實現

**_WaitSet ** :處於wait狀態的執行緒,會被加入到wait set;
_EntryList:處於等待鎖block狀態的執行緒,會被加入到entry set;

ObjectWaiter

JVM原始碼分析之Object.wait/notify實現

ObjectWaiter物件是雙向連結串列結構,儲存了_thread(當前執行緒)以及當前的狀態TState等資料, 每個等待鎖的執行緒都會被封裝成ObjectWaiter物件。

wait方法實現

lock.wait()方法最終通過ObjectMonitor的void wait(jlong millis, bool interruptable, TRAPS);實現:
1、將當前執行緒封裝成ObjectWaiter物件node;

JVM原始碼分析之Object.wait/notify實現

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

JVM原始碼分析之Object.wait/notify實現

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

JVM原始碼分析之Object.wait/notify實現
4、最終底層的park方法會掛起執行緒;

notify方法實現

lock.notify()方法最終通過ObjectMonitor的void notify(TRAPS)實現:
1、如果當前_WaitSet為空,即沒有正在等待的執行緒,則直接返回;
2、通過ObjectMonitor::DequeueWaiter方法,獲取_WaitSet列表中的第一個ObjectWaiter節點,實現也很簡單。
這裡需要注意的是,在jdk的notify方法註釋是隨機喚醒一個執行緒,其實是第一個ObjectWaiter節點

JVM原始碼分析之Object.wait/notify實現

3、根據不同的策略,將取出來的ObjectWaiter節點,加入到_EntryList或則通過Atomic::cmpxchg_ptr指令進行自旋操作cxq,具體程式碼實現有點長,這裡就不貼了,有興趣的同學可以看objectMonitor::notify方法;

notifyAll方法實現

lock.notifyAll()方法最終通過ObjectMonitor的void notifyAll(TRAPS)實現:
通過for迴圈取出_WaitSet的ObjectWaiter節點,並根據不同策略,加入到_EntryList或則進行自旋操作。

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

覺得不錯請點贊支援,歡迎留言或進我的個人群855801563領取【架構資料專題目合集90期】、【BATJTMD大廠JAVA面試真題1000+】,本群專用於學習交流技術、分享面試機會,拒絕廣告,我也會在群內不定期答題、探討。


相關文章