啃碎併發(八):深入分析wait&notify原理

猿碼道發表於2018-08-16

0 前言

上一節講了Synchronized關鍵詞的原理與優化分析,而配合Synchronized使用的另外兩個關鍵詞wait&notify是本章講解的重點。最簡單的東西,往往包含了最複雜的實現,因為需要為上層的存在提供一個穩定的基礎,Object作為Java中所有物件的基類,其存在的價值不言而喻,其中wait&notify方法的實現多執行緒協作提供了保證

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 程式碼執行的。有 2 個 wait 過載方法最終還是呼叫了 wait(long) 方法。

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

  2. notify方法:也需要寫在synchronized程式碼塊中,呼叫物件的這兩個方法也需要先獲得該物件的鎖。notify,notifyAll,喚醒等待該物件同步鎖的執行緒。notify喚醒物件等待池中的一個執行緒,將這個執行緒放入該物件的鎖池中。物件的鎖池中執行緒可以去競爭得到物件鎖,然後開始執行

    1. 如果是通過notify來喚起的執行緒,那先進入wait的執行緒會先被喚起來,並非隨機喚醒;
    2. 如果是通過nootifyAll喚起的執行緒,預設情況是最後進入的會先被喚起來,即LIFO的策略;

    另一點,notify,notifyAll呼叫時並不會釋放物件鎖。比如以下程式碼:

    public void test()
    {
        Object object = new Object();
        synchronized (object){
            object.notifyAll();
            while (true){
             
            }
        }
    }
    複製程式碼

    雖然呼叫了notifyAll,但是緊接著進入了一個死迴圈。導致一直不能出臨界區,一直不能釋放物件鎖。所以,即使它把所有在等待池中的執行緒都喚醒放到了物件的鎖池中,但是鎖池中的所有執行緒都不會執行,因為他們都拿不到鎖

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();
    }
}
複製程式碼

執行結果:

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繼續執行;

相關文章