1. 程式人生 > 實用技巧 >執行緒 等待/通知機制

執行緒 等待/通知機制

4.3.2 等待/通知機制

一個執行緒修改了一個物件的值,而另一個執行緒感知到了變化,然後進行相應的操作,整個過程開始於一個執行緒,而最終執行又是另一個執行緒。前者是生產者,後者就是消費者,這種模式隔離了“做什麼”(what)和“怎麼做”(How),在功能層面上實現瞭解耦,體系結構上具備了良好的伸縮性,但是在Java語言中如何實現類似的功能呢?

簡單的辦法是讓消費者執行緒不斷地迴圈檢查變數是否符合預期,如下面程式碼所示,在while迴圈中設定不滿足的條件,如果條件滿足則退出while迴圈,從而完成消費者的工作。

上面這段虛擬碼在條件不滿足時就睡眠一段時間,這樣做的目的是防止過快的“無效”嘗試,這種方式看似能夠解實現所需的功能,但是卻存在如下問題。

1)難以確保及時性。在睡眠時,基本不消耗處理器資源,但是如果睡得過久,就不能及時發現條件已經變化,也就是及時性難以保證。

2)難以降低開銷。如果降低睡眠的時間,比如休眠1毫秒,這樣消費者能更加迅速地發現條件變化,但是卻可能消耗更多的處理器資源,造成了無端的浪費。

以上兩個問題,看似矛盾難以調和,但是Java通過內建的等待/通知機制能夠很好地解決這個矛盾並實現所需的功能。

等待/通知的相關方法是任意Java物件都具備的,因為這些方法被定義在所有物件的超類java.lang.Object上,方法和描述如表4-2所示。

表4-2 等待/通知的相關方法

等待/通知機制,是指一個執行緒A呼叫了物件O的wait()方法進入等待狀態,而另一個執行緒B呼叫了物件O的notify()或者notifyAll()方法,執行緒A收到通知後從物件O的wait()方法返回,進而執行後續操作。上述兩個執行緒通過物件O來完成互動,而物件上的wait()和notify/notifyAll()的關係就如同開關訊號一樣,用來完成等待方和通知方之間的互動工作。

程式碼清單4-11 WaitNotify.java

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;

public class b4_3_2等待通知機制 {
}

class WaitNotify {
    static boolean flag = true;
    static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread waitThread 
= new Thread(new Wait(), "WaitThread"); waitThread.start(); TimeUnit.SECONDS.sleep(1); Thread notifyThread = new Thread(new Notify(), "NotifyThread"); notifyThread.start(); } static class Wait implements Runnable { public void run() { // 加鎖,擁有lock的Monitor synchronized (lock) { // 當條件不滿足時,繼續wait,同時釋放了lock的鎖 while (flag) { try { System.out.println(Thread.currentThread() + " flag is true. wait @ " + new SimpleDateFormat(" HH:mm:ss ").format(new Date())); lock.wait(); } catch (InterruptedException e) { } } // 條件滿足時,完成工作 System.out.println(Thread.currentThread() + " flag is false. running@ " + new SimpleDateFormat(" HH:mm:ss ").format(new Date())); } } } static class Notify implements Runnable { @Override public void run() { // 加鎖,擁有lock的Monitor synchronized (lock) { // 獲取lock的鎖,然後進行通知,通知時不會釋放lock的鎖, // 直到當前執行緒釋放了lock後,WaitThread才能從wait方法中返回 System.out.println(Thread.currentThread() + " hold lock. notify @ " + new SimpleDateFormat(" HH:mm:ss ").format(new Date())); lock.notifyAll(); flag = false; SleepUtils.second(5); } // 再次加鎖 synchronized (lock){ System.out.println(Thread.currentThread() + " hold lock again. sleep @ " + new SimpleDateFormat(" HH:mm:ss ").format(new Date())); SleepUtils.second(5); } } } }
Thread[WaitThread,5,main] flag is true. wait @  08:50:55 
Thread[NotifyThread,5,main] hold lock. notify @  08:50:56 
Thread[NotifyThread,5,main] hold lock again. sleep @  08:51:01 
Thread[WaitThread,5,main] flag is false. running@  08:51:06 

上述第3行和第4行輸出的順序可能會互換,而上述例子主要說明了呼叫wait()、notify()以及notifyAll()時需要注意的細節,如下。

1)使用wait()、notify()和notifyAll()時需要先對呼叫物件加鎖。

2)呼叫wait()方法後,執行緒狀態由RUNNING變為WAITING,並將當前執行緒放置到物件的等待佇列。

3)notify()或notifyAll()方法呼叫後,等待執行緒依舊不會從wait()返回,需要呼叫notify()或notifAll()的執行緒釋放鎖之後,等待執行緒才有機會從wait()返回。

4)notify()方法將等待佇列中的一個等待執行緒從等待佇列中移到同步佇列中,而notifyAll()方法則是將等待佇列中所有的執行緒全部移到同步佇列,被移動的執行緒狀態由WAITING變為BLOCKED。

5)從wait()方法返回的前提是獲得了呼叫物件的鎖。

從上述細節中可以看到,等待/通知機制依託於同步機制,其目的就是確保等待執行緒從wait()方法返回時能夠感知到通知執行緒對變數做出的修改。

圖4-3描述了上述示例的過程。

在圖4-3中,WaitThread首先獲取了物件的鎖,然後呼叫物件的wait()方法,從而放棄了鎖並進入了物件的等待佇列WaitQueue中,進入等待狀態。由於WaitThread釋放了物件的鎖,NotifyThread隨後獲取了物件的鎖,並呼叫物件的notify()方法,將WaitThread從WaitQueue移到SynchronizedQueue中,此時WaitThread的狀態變為阻塞狀態。NotifyThread釋放了鎖之後,WaitThread再次獲取到鎖並從wait()方法返回繼續執行。

4.3.3 等待/通知的經典範式

從4.3.2節中的WaitNotify示例中可以提煉出等待/通知的經典範式,該正規化分為兩部分,分別針對等待方(消費者)和通知方(生產者)。

等待方遵循如下原則。

1)獲取物件的鎖。

2)如果條件不滿足,那麼呼叫物件的wait()方法,被通知後仍要檢查條件。

3)條件滿足則執行對應的邏輯。對應的虛擬碼如下。

通知方遵循如下原則。

1)獲得物件的鎖。

2)改變條件。

3)通知所有等待在物件上的執行緒。

對應的虛擬碼如下。

來源:java併發程式設計的藝術 4.3.2