1. 程式人生 > >【Java】多執行緒系列05(執行緒等待與喚醒)

【Java】多執行緒系列05(執行緒等待與喚醒)

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

在Object.java中,定義了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() 方法,或者其他某個執行緒中斷當前執行緒,或者已超過某個實際時間量”,當前執行緒被喚醒(進入“就緒狀態”)。

2、wait()和notify()示例

下面通過示例演示”wait()和notify()配合使用的情形”。

// WaitTest.java的原始碼
class ThreadA extends Thread{

    public ThreadA(String name) {
        super(name);
    }

    public void run() {
        synchronized (this) {
            System.out.println(Thread.currentThread().getName()+" call notify()"
); // 喚醒當前的wait執行緒 notify(); } } } public class WaitTest { public static void main(String[] args) { ThreadA t1 = new ThreadA("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(); } } } }

執行結果:

main start t1
main wait()
t1 call notify()
main continue

結果說明:
如下圖,說明了“主執行緒”和“執行緒t1”的流程。

(01)注意,圖中”主執行緒” 代表“主執行緒main”。”執行緒t1” 代表WaitTest中啟動的“執行緒t1”。 而“鎖” 代表“t1這個物件的同步鎖”。
(02)“主執行緒”通過 new ThreadA(“t1”) 新建“執行緒t1”。隨後通過synchronized(t1)獲取“t1物件的同步鎖”。然後呼叫t1.start()啟動“執行緒t1”。
(03)“主執行緒”執行t1.wait() 釋放“t1物件的鎖”並且進入“等待(阻塞)狀態”。等待t1物件上的執行緒通過notify() 或 notifyAll()將其喚醒。
(04)“執行緒t1”執行之後,通過synchronized(this)獲取“當前物件的鎖”;接著呼叫notify()喚醒“當前物件上的等待執行緒”,也就是喚醒“主執行緒”。
(05)“執行緒t1”執行完畢之後,釋放“當前物件的鎖”。緊接著,“主執行緒”獲取“t1物件的鎖”,然後接著執行。
這裡寫圖片描述
對於上面的程式碼?曾經有個朋友問到過:t1.wait()應該是讓“執行緒t1”等待;但是,為什麼卻是讓“主執行緒main”等待了呢?
在解答該問題前,我們先看看jdk文件中關於wait的一段介紹:

引起“當前執行緒”等待,直到另外一個執行緒呼叫notify()或notifyAll()喚醒該執行緒。換句話說,這個方法和wait(0)的效果一樣!(補充,對於wait(long millis)方法,當millis為0時,表示無限等待,直到被notify()或notifyAll()喚醒)。
“當前執行緒”在呼叫wait()時,必須擁有該物件的同步鎖。該執行緒呼叫wait()之後,會釋放該鎖;然後一直等待直到“其它執行緒”呼叫物件的同步鎖的notify()或notifyAll()方法。然後,該執行緒繼續等待直到它重新獲取“該物件的同步鎖”,然後就可以接著執行。
**注意:**jdk的解釋中,說wait()的作用是讓“當前執行緒”等待,而“當前執行緒”是指正在cpu上執行的執行緒!
這也意味著,雖然t1.wait()是通過“執行緒t1”呼叫的wait()方法,但是呼叫t1.wait()的地方是在“主執行緒main”中。而主執行緒必須是“當前執行緒”,也就是執行狀態,才可以執行t1.wait()。所以,此時的“當前執行緒”是“主執行緒main”!因此,t1.wait()是讓“主執行緒”等待,而不是“執行緒t1”。

3、wait(long timeout)和notify()

wait(long timeout)會讓當前執行緒處於“等待(阻塞)狀態”,“直到其他執行緒呼叫此物件的 notify() 方法或 notifyAll() 方法,或者超過指定的時間量”,當前執行緒被喚醒(進入“就緒狀態”)。
下面的示例就是演示wait(long timeout)在超時情況下,執行緒被喚醒的情況。

// WaitTimeoutTest.java的原始碼
class ThreadA extends Thread{

    public ThreadA(String name) {
        super(name);
    }

    public void run() {
        System.out.println(Thread.currentThread().getName() + " run ");
        // 死迴圈,不斷執行。
        while(true)
            ;
    }
}

public class WaitTimeoutTest {

    public static void main(String[] args) {

        ThreadA t1 = new ThreadA("t1");

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

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

                System.out.println(Thread.currentThread().getName() + " continue");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

執行結果:

main start t1
main call wait 
t1 run                  // 大約3秒之後...輸出“main continue”
main continue

結果說明:
如下圖,說明了“主執行緒”和“執行緒t1”的流程。
(01)注意,圖中”主執行緒” 代表WaitTimeoutTest主執行緒(即,執行緒main)。”執行緒t1” 代表WaitTest中啟動的執行緒t1。 而“鎖” 代表“t1這個物件的同步鎖”。
(02)主執行緒main執行t1.start()啟動“執行緒t1”。
(03)主執行緒main執行t1.wait(3000),此時,主執行緒進入“阻塞狀態”。需要“用於t1物件鎖的執行緒通過notify() 或者 notifyAll()將其喚醒” 或者 “超時3000ms之後”,主執行緒main才進入到“就緒狀態”,然後才可以執行。
(04)“執行緒t1”執行之後,進入了死迴圈,一直不斷的執行。
(05)超時3000ms之後,主執行緒main會進入到“就緒狀態”,然後接著進入“執行狀態”。
這裡寫圖片描述

4、wait() 和 notifyAll()

通過前面的示例,我們知道 notify() 可以喚醒在此物件監視器上等待的單個執行緒。
下面,我們通過示例演示notifyAll()的用法;它的作用是喚醒在此物件監視器上等待的所有執行緒。

public class NotifyAllTest {

    private static Object obj = new Object();
    public static void main(String[] args) {

        ThreadA t1 = new ThreadA("t1");
        ThreadA t2 = new ThreadA("t2");
        ThreadA t3 = new ThreadA("t3");
        t1.start();
        t2.start();
        t3.start();

        try {
            System.out.println(Thread.currentThread().getName()+" sleep(3000)");
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        synchronized(obj) {
            // 主執行緒等待喚醒。
            System.out.println(Thread.currentThread().getName()+" notifyAll()");
            obj.notifyAll();
        }
    }

    static class ThreadA extends Thread{

        public ThreadA(String name){
            super(name);
        }

        public void run() {
            synchronized (obj) {
                try {
                    // 列印輸出結果
                    System.out.println(Thread.currentThread().getName() + " wait");

                    // 喚醒當前的wait執行緒
                    obj.wait();

                    // 列印輸出結果
                    System.out.println(Thread.currentThread().getName() + " continue");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

執行結果:

t1 wait
main sleep(3000)
t3 wait
t2 wait
main notifyAll()
t2 continue
t3 continue
t1 continue

結果說明:
參考下面的流程圖。
(01)主執行緒中新建並且啟動了3個執行緒”t1”, “t2”和”t3”。
(02)主執行緒通過sleep(3000)休眠3秒。在主執行緒休眠3秒的過程中,我們假設”t1”, “t2”和”t3”這3個執行緒都運行了。以”t1”為例,當它執行的時候,它會執行obj.wait()等待其它執行緒通過notify()或額nofityAll()來喚醒它;相同的道理,”t2”和”t3”也會等待其它執行緒通過nofity()或nofityAll()來喚醒它們。
(03)主執行緒休眠3秒之後,接著執行。執行 obj.notifyAll() 喚醒obj上的等待執行緒,即喚醒”t1”, “t2”和”t3”這3個執行緒。 緊接著,主執行緒的synchronized(obj)執行完畢之後,主執行緒釋放“obj鎖”。這樣,”t1”, “t2”和”t3”就可以獲取“obj鎖”而繼續運行了!
這裡寫圖片描述

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

Object中的wait(), notify()等函式,和synchronized一樣,會對“物件的同步鎖”進行操作。

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

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

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