Java 多執行緒基礎(六)執行緒等待與喚醒

 Java 多執行緒基礎(六)執行緒等待與喚醒

遇到這樣一個場景,當某執行緒裡面的邏輯需要等待非同步處理結果返回後才能繼續執行。或者說想要把一個非同步的操作封裝成一個同步的過程。這裡就用到了執行緒等待喚醒機制。

wait()、notify()、notifyAll() 等方法介紹

在 Object 中,定義了 wait()、notify() 和 notifyAll() 等介面。wait() 的作用是讓當前執行緒進入等待狀態,同時,wait() 也會讓當前執行緒釋放它所持有的鎖。而 notify() 和 notifyAll() 的作用,則是喚醒當前對象上的等待執行緒;notify() 是喚醒單個執行緒,而 notifyAll() 是喚醒所有的執行緒。

Object類中關於等待/喚醒的API詳細資訊如下:

notify()                                       — 喚醒在此對象監視器上等待的單個執行緒。
notifyAll()                                  — 喚醒在此對象監視器上等待的所有執行緒。
wait()                                         — 讓當前執行緒處於「等待(阻塞)狀態」,「直到其他執行緒調用此對象的 notify() 方法或 notifyAll() 方法」,當前執行緒被喚醒(進入「就緒狀態」)。
wait(long timeout)                    — 讓當前執行緒處於「等待(阻塞)狀態」,「直到其他執行緒調用此對象的 notify() 方法或 notifyAll() 方法,或者超過指定的時間量」,當前執行緒被喚醒(進入「就緒狀態」)。
wait(long timeout, int nanos)  — 讓當前執行緒處於「等待(阻塞)狀態」,「直到其他執行緒調用此對象的 notify() 方法或 notifyAll() 方法,或者其他某個執行緒中斷當前執行緒,或者已超過某個實際時間量」,當前執行緒被喚醒(進入「就緒狀態」)。

二、wait() 和 notify() 示例

public class Demo02 {
    public static void main(String[] args) {
        Thread t1 = new MyThread("t1");
        synchronized (t1) {
            try {
                // 啟動「執行緒t1」
                System.out.println(Thread.currentThread().getName()+" start t1");
                t1.start();
                // 主執行緒等待t1通過notify()喚醒。
                System.out.println(Thread.currentThread().getName()+" wait()");
                t1.wait();
                System.out.println(Thread.currentThread().getName()+" continue");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
class MyThread extends Thread{
    public MyThread(String name) {
        super(name);
    }
    @Override
    public void run() {
        synchronized (this) {
            try {
                System.out.println(Thread.currentThread().getName()+" call notify()");
                notify(); // 喚醒當前的Demo02執行緒
            }catch(Exception e) {
                e.printStackTrace();
            }
        }
    }
}
// 運行結果
main start t1
main wait()
t1 call notify()
main continue

說明:

①、 注意,圖中”主執行緒” 代表「主執行緒main」。”執行緒t1″ 代表Demo02中啟動的「執行緒t1」。 而「鎖」 代表「t1這個對象的同步鎖」。
②、「主執行緒」通過 new ThreadA(“t1”) 新建「執行緒t1」。隨後通過synchronized(t1)獲取「t1對象的同步鎖」。然後調用t1.start()啟動「執行緒t1」。
③、「主執行緒」執行t1.wait() 釋放「t1對象的鎖」並且進入「等待(阻塞)狀態」。等待t1對象上的執行緒通過notify() 或 notifyAll()將其喚醒。
④、「執行緒t1」運行之後,通過synchronized(this)獲取「當前對象的鎖」;接著調用notify()喚醒「當前對象上的等待執行緒」,也就是喚醒「主執行緒」。
⑤、「執行緒t1」運行完畢之後,釋放「當前對象的鎖」。緊接著,「主執行緒」獲取「t1對象的鎖」,然後接著運行。

具體過程圖解

三、wait(long timeout) 和 notify()

public class Demo02 {
    public static void main(String[] args) {
        Thread t1 = new MyThread("t1");

        synchronized(t1) {
            try {
                // 啟動執行緒t1
                System.out.println(Thread.currentThread().getName() + " start t1");
                t1.start();

                // 主執行緒等待t1通過notify()喚醒 或 notifyAll()喚醒,或超過3s延時;然後才被喚醒。
                System.out.println(Thread.currentThread().getName() + " call wait ");
                t1.wait(3000);

                System.out.println(Thread.currentThread().getName() + " continue");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
    }
}
class MyThread extends Thread{
    public MyThread(String name) {
        super(name);
    }
    public void run() {
        System.out.println(Thread.currentThread().getName() + " run ");
        // 死循環,不斷運行。
        while(true)
            ;
    }
}
// 運行結果
main start t1
main call wait 
t1 run             // 3秒後輸出 main continue
main continue 

 說明:

如下圖,說明了「主執行緒」和「執行緒t1」的流程。
①、注意,圖中”主執行緒” 代表執行緒main。”執行緒t1″ 代表MyThread中啟動的執行緒t1。 而「鎖」 代表「t1這個對象的同步鎖」。
②、主執行緒main執行t1.start()啟動「執行緒t1」。
③、主執行緒main執行t1.wait(3000),此時,主執行緒進入「阻塞狀態」。需要「用於t1對象鎖的執行緒通過notify() 或者 notifyAll()將其喚醒」 或者 「超時3000ms之後」,主執行緒main才進入到「就緒狀態」,然後才可以運行。
④、「執行緒t1」運行之後,進入了死循環,一直不斷的運行。
⑤、超時3000ms之後,主執行緒main會進入到「就緒狀態」,然後接著進入「運行狀態」。

 具體過程圖解:

四、wait() 和 notifyAll()

public class Demo02 {
    private static Object obj = new Object();
    public static void main(String[] args) {
        MyThread t1 = new MyThread("t1");
        MyThread t2 = new MyThread("t2");
        MyThread t3 = new MyThread("t3");
        t1.start();
        t2.start();
        t3.start();
        try {
            System.out.println(Thread.currentThread().getName()+" sleep(5000)");
            Thread.sleep(5000); // 休眠5秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (obj) {
            System.out.println(Thread.currentThread().getName()+" notifyAll()");
            obj.notifyAll();
        }
        
    }
    static class MyThread extends Thread{
        public MyThread(String name) {
            super(name);
        }
        public void run() {
            synchronized (obj) { 
                try {
                    System.out.println(Thread.currentThread().getName() + " run ");
                    obj.wait();
                    System.out.println(Thread.currentThread().getName() + " continue");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
// 運行結果
t1 run 
t2 run 
main sleep(5000)
t3 run 
main notifyAll()
t3 continue
t2 continue
t1 continue

說明:

①、 主執行緒中新建並且啟動了3個執行緒”t1″, “t2″和”t3″。
②、主執行緒通過sleep(5000)休眠5秒。在主執行緒休眠3秒的過程中,我們假設”t1″, “t2″和”t3″這3個執行緒都運行了。以”t1″為例,當它運行的時候,它會執行obj.wait()等待其它執行緒通過notify()或nofityAll()來喚醒它;相同的道理,”t2″和”t3″也會等待其它執行緒通過nofity()或nofityAll()來喚醒它們。
③、主執行緒休眠3秒之後,接著運行。執行 obj.notifyAll() 喚醒obj上的等待執行緒,即喚醒”t1″, “t2″和”t3″這3個執行緒。 緊接著,主執行緒的synchronized(obj)運行完畢之後,主執行緒釋放「obj鎖」。這樣,”t1”, “t2″和”t3″就可以獲取「obj鎖」而繼續運行了!

具體過程圖解

五、 為什麼notify(), wait()等函數定義在Object中,而不是Thread中

Object中的wait(), notify()等函數,和synchronized一樣,會對「對象的同步鎖」進行操作。

wait()會使「當前執行緒」等待,因為執行緒進入等待狀態,所以執行緒應該釋放它鎖持有的「同步鎖」,否則其它執行緒獲取不到該「同步鎖」而無法運行!
OK,執行緒調用wait()之後,會釋放它鎖持有的「同步鎖」;而且,根據前面的介紹,我們知道:等待執行緒可以被notify()或notifyAll()喚醒。現在,請思考一個問題:notify()是依據什麼喚醒等待執行緒的?或者說,wait()等待執行緒和notify()之間是通過什麼關聯起來的?答案是:依據「對象的同步鎖」。

負責喚醒等待執行緒的那個執行緒(我們稱為「喚醒執行緒」),它只有在獲取「該對象的同步鎖」(這裡的同步鎖必須和等待執行緒的同步鎖是同一個),並且調用notify()或notifyAll()方法之後,才能喚醒等待執行緒。雖然,等待執行緒被喚醒;但是,它不能立刻執行,因為喚醒執行緒還持有「該對象的同步鎖」。必須等到喚醒執行緒釋放了「對象的同步鎖」之後,等待執行緒才能獲取到「對象的同步鎖」進而繼續運行。

總之,notify(), wait()依賴於「同步鎖」,而「同步鎖」是對象鎖持有,並且每個對象有且僅有一個!這就是為什麼notify(), wait()等函數定義在Object類,而不是Thread類中的原因。