☆啃碎并發(fā)(八):深入分析wait&notify原理

0 前言

上一節(jié)講了Synchronized關(guān)鍵詞的原理與優(yōu)化分析,而配合Synchronized使用的另外兩個關(guān)鍵詞wait&notify是本章講解的重點。最簡單的東西,往往包含了最復雜的實現(xiàn),因為需要為上層的存在提供一個穩(wěn)定的基礎(chǔ),Object作為Java中所有對象的基類,其存在的價值不言而喻,其中wait&notify方法的實現(xiàn)多線程協(xié)作提供了保證。

1 源碼

今天我們要學習或者說分析的是 Object 類中的 wait&notify 這兩個方法,其實說是兩個方法,這兩個方法包括他們的重載方法一共有 5 個,而 Object 類中一共才 12 個方法,可見這 2 個方法的重要性。我們先看看 JDK 中的代碼:

public final native void notify();

public final native void notifyAll();

public final void wait() throws InterruptedException {
    wait(0);
}

public final native void wait(long timeout) throws InterruptedException;

public final void wait(long timeout, int nanos) throws InterruptedException {
    if (timeout < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (nanos < 0 || nanos > 999999) {
        throw new IllegalArgumentException(
                            "nanosecond timeout value out of range");
    }
    // 此處對于納秒的處理不精準,只是簡單增加了1毫秒,
    if (nanos > 0) {
        timeout++;
    }

    wait(timeout);
}

就是這五個方法。其中有 3 個方法是 native 的,也就是由虛擬機本地的 c 代碼執(zhí)行的。有 2 個 wait 重載方法最終還是調(diào)用了 wait(long) 方法。

  1. wait方法:wait是要釋放對象鎖,進入等待池。既然是釋放對象鎖,那么肯定是先要獲得鎖。所以wait必須要寫在synchronized代碼塊中,否則會報異常。

  2. notify方法:也需要寫在synchronized代碼塊中,調(diào)用對象的這兩個方法也需要先獲得該對象的鎖。notify,notifyAll,喚醒等待該對象同步鎖的線程,并放入該對象的鎖池中。對象的鎖池中線程可以去競爭得到對象鎖,然后開始執(zhí)行。

    1. 如果是通過notify來喚起的線程,那先進入wait的線程會先被喚起來,并非隨機喚醒;
    2. 如果是通過nootifyAll喚起的線程,默認情況是最后進入的會先被喚起來,即LIFO的策略;

    另外一點比較重要,notify,notifyAll調(diào)用時并不會釋放對象鎖。比如以下代碼:

    public void test()
    {
        Object object = new Object();
        synchronized (object){
            object.notifyAll();
            while (true){
             
            }
        }
    }
    

    雖然調(diào)用了notifyAll,但是緊接著進入了一個死循環(huán)。導致一直不能出臨界區(qū),一直不能釋放對象鎖。所以,即使它把所有在等待池中的線程都喚醒放到了對象的鎖池中,但是鎖池中的所有線程都不會運行,因為他們始終拿不到鎖

2 用法

簡單示例:

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();
    }
}

執(zhí)行結(jié)果:

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對象調(diào)用wait、notify方法

  1. 當線程A執(zhí)行wait方法時,該線程會被掛起;
  2. 當線程B執(zhí)行notify方法時,會喚醒一個被掛起的線程A;

lock對象、線程A和線程B三者是一種什么關(guān)系?根據(jù)上面的結(jié)論,可以想象一個場景:

  1. lock對象維護了一個等待隊列l(wèi)ist;
  2. 線程A中執(zhí)行l(wèi)ock的wait方法,把線程A保存到list中;
  3. 線程B中執(zhí)行l(wèi)ock的notify方法,從等待隊列中取出線程A繼續(xù)執(zhí)行;

3 相關(guān)疑問

3.1 為何wait&notify必須要加synchronized鎖

從實現(xiàn)上來說,這個鎖至關(guān)重要,正因為這把鎖,才能讓整個wait/notify玩轉(zhuǎn)起來,當然我覺得其實通過其他的方式也可以實現(xiàn)類似的機制,不過hotspot至少是完全依賴這把鎖來實現(xiàn)wait/notify的

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生成的字節(jié)碼中包含 monitorentermonitorexit 指令。如下圖所示:

javap生成的字節(jié)碼

執(zhí)行 monitorenter 指令可以獲取對象的monitor,而 lock.wait() 方法通過調(diào)用native方法wait(0)實現(xiàn),其中接口注釋中有這么一句:

The current thread must own this object's monitor.

表示線程執(zhí)行 lock.wait() 方法時,必須持有該lock對象的monitor,如果wait方法在synchronized代碼中執(zhí)行,該線程很顯然已經(jīng)持有了monitor。

3.2 為什么wait方法可能拋出InterruptedException異常

這個異常大家應該都知道,當我們調(diào)用了某個線程的interrupt方法時,對應的線程會拋出這個異常,wait方法也不希望破壞這種規(guī)則,因此就算當前線程因為wait一直在阻塞,當某個線程希望它起來繼續(xù)執(zhí)行的時候,它還是得從阻塞態(tài)恢復過來,因此wait方法被喚醒起來的時候會去檢測這個狀態(tài),當有線程interrupt了它的時候,它就會拋出這個異常從阻塞狀態(tài)恢復過來。

這里有兩點要注意:

  1. 如果被interrupt的線程只是創(chuàng)建了,并沒有start,那等他start之后進入wait態(tài)之后也是不能會恢復的;

  2. 如果被interrupt的線程已經(jīng)start了,在進入wait之前,如果有線程調(diào)用了其interrupt方法,那這個wait等于什么都沒做,會直接跳出來,不會阻塞;

3.3 notify執(zhí)行之后立馬喚醒線程嗎

其實hotspot里真正的實現(xiàn)是退出同步塊的時候才會去真正喚醒對應的線程,不過這個也是個默認策略,也可以改的,在notify之后立馬喚醒相關(guān)線程。

3.4 notifyAll是怎么實現(xiàn)全喚起所有線程

或許大家立馬想到這個簡單,一個for循環(huán)就搞定了,不過在JVM里沒實現(xiàn)這么簡單,而是借助了monitorexit,上面提到了當某個線程從wait狀態(tài)恢復出來的時候,要先獲取鎖,然后再退出同步塊,所以notifyAll的實現(xiàn)是調(diào)用notify的線程在退出其同步塊的時候喚醒起最后一個進入wait狀態(tài)的線程,然后這個線程退出同步塊的時候繼續(xù)喚醒其倒數(shù)第二個進入wait狀態(tài)的線程,依次類推,同樣這這是一個策略的問題,JVM里提供了挨個直接喚醒線程的參數(shù),不過都很罕見就不提了。

3.5 wait的線程是否會影響load

這個或許是大家比較關(guān)心的話題,因為關(guān)乎系統(tǒng)性能問題,wait/nofity 是通過JVM里的 park/unpark 機制來實現(xiàn)的,在Linux下這種機制又是通過
pthread_cond_wait/pthread_cond_signal 來玩的
,因此當線程進入到wait狀態(tài)的時候其實是會放棄cpu的,也就是說這類線程是不會占用cpu資源。

4 其他資料

  1. Java的wait()、notify()學習三部曲之一:JVM源碼分析
  2. Java的wait()、notify()學習三部曲之二:修改JVM源碼看參數(shù)
  3. Java的wait()、notify()學習三部曲之三:修改JVM源碼控制搶鎖順序
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容