等待/通知機制 wait/notify/notifyAll 方法

線程間的協(xié)作

線程之間相互配合,完成某項工作,比如:一個線程修改了一個對象的值, 而另一個線程感知到了變化,然后進行相應(yīng)的操作,整個過程開始于一個線程, 而最終執(zhí)行又是另一個線程。前者是生產(chǎn)者,后者就是消費者,這種模式隔離了 “做什么”(what)和“怎么做”(How),簡單的辦法是讓消費者線程不斷地循環(huán)檢查變量是否符合預(yù)期在 while 循環(huán)中設(shè)置不滿足的條件,如果條件滿足則退出 while 循環(huán),從而完成消費者的工作。卻存在如下問題:
1) 難以確保及時性。
2)難以降低開銷。如果降低睡眠的時間,比如休眠 1 毫秒,這樣消費者能 更加迅速地發(fā)現(xiàn)條件變化,但是卻可能消耗更多的處理器資源,造成了無端的浪 費。

等待/通知機制

是指一個線程 A 調(diào)用了對象 O 的 wait()方法進入等待狀態(tài),而另一個線程 B 調(diào)用了對象 O 的 notify()或者 notifyAll()方法,線程 A 收到通知后從對象 O 的 wait() 方法返回,進而執(zhí)行后續(xù)操作。上述兩個線程通過對象 O 來完成交互,而對象 上的 wait()和 notify/notifyAll()的關(guān)系就如同開關(guān)信號一樣,用來完成等待方和通 知方之間的交互工作。

  • notify()
    通知一個在對象上等待的線程,使其從 wait 方法返回,而返回的前提是該線程 獲取到了對象的鎖,沒有獲得鎖的線程重新進入 WAITING 狀態(tài)。
  • notifyAll()
    通知所有等待在該對象上的線程
  • wait()
    調(diào)用該方法的線程進入 WAITING 狀態(tài),只有等待另外線程的通知或被中斷才會返回。需要注意,調(diào)用 wait()方法后,會釋放對象的鎖
  • wait(long)
    超時等待一段時間,這里的參數(shù)時間是毫秒,也就是等待長達 n 毫秒,如果沒有通知就超時返回
  • wait (long,int)
    對于超時時間更細粒度的控制,可以達到納秒

等待和通知的標準范式
等待方遵循如下原則。
1)獲取對象的鎖。
2)如果條件不滿足,那么調(diào)用對象的 wait() 方法,被通知后仍要檢查條件。
3)條件滿足則執(zhí)行對應(yīng)的邏輯。

通知方遵循如下原則。
1)獲得對象的鎖。
2)改變條件。
3)通知所有等待在對象上的線程。

在調(diào)用 wait()、notify() 系列方法之前,線程必須要獲得該對象的對象級別鎖,即只能在同步方法或同步塊中調(diào)用 wait() 方法、notify()系列方法,進入 wait() 方法后,當前線程釋放鎖,在從 wait() 返回前,線程與其他線程競爭重新獲得鎖,執(zhí)行 notify() 系列方法的線程退出調(diào)用了 notifyAll 的 synchronized 代碼塊的時候后,他們就會去競爭。如果其中一個線程獲得了該對象鎖,它就會繼續(xù)往下執(zhí)行,在它退出 synchronized 代碼塊,釋放鎖后,其他的已經(jīng)被喚醒的線程將會繼續(xù)競爭獲取該鎖,一直進行下去,直到所有被喚醒的線程都執(zhí)行完畢。

notify 和 notifyAll 應(yīng)該用誰
盡可能用 notifyall(),謹慎使用 notify(),因為 notify() 只會喚醒一個線程,我們無法確保被喚醒的這個線程一定就是我們需要喚醒的線程

wait/notify/notifyAll 方法的使用注意事項。

我們主要從三個問題入手:

1. 為什么 wait 方法必須在 synchronized 保護的同步代碼中使用?
2. 為什么 wait/notify/notifyAll 被定義在 Object 類中,而 sleep 定義在 Thread 類中?
3. wait/notify 和 sleep 方法的異同?

為什么 wait 方法必須在 synchronized 保護的同步代碼中使用?

先來看看 wait 方法的源碼注釋是怎么寫的。

“wait method should always be used in a loop:

 synchronized (obj) {
     while (condition does not hold)
         obj.wait();
     ... // Perform action appropriate to condition
}

This method should only be called by a thread that is the owner of this object's monitor.”
意思是說,在使用 wait 方法時,必須把 wait 方法寫在 synchronized 保護的 while 代碼塊中,并始終判斷執(zhí)行條件是否滿足,如果滿足就往下繼續(xù)執(zhí)行,如果不滿足就執(zhí)行 wait 方法,而在執(zhí)行 wait 方法之前,必須先持有對象的 monitor 鎖,也就是通常所說的 synchronized 鎖。

如果不要求 wait 方法放在 synchronized 保護的同步代碼中使用,而是可以隨意調(diào)用,實例代碼如下:

class BlockingQueue {
    Queue<String> buffer = new LinkedList<String>();
    public void give(String data) {
        buffer.add(data);
        notify();  // Since someone may be waiting in take
    }

    public String take() throws InterruptedException {
        while (buffer.isEmpty()) {
            wait();
        }
        return buffer.remove();
    }
}

give 方法負責(zé)往 buffer 中添加數(shù)據(jù),添加完之后執(zhí)行 notify 方法來喚醒之前等待的線程,而 take 方法負責(zé)檢查整個 buffer 是否為空,如果為空就進入等待,如果不為空就取出一個數(shù)據(jù),這是典型的生產(chǎn)者消費者的思想。

這段代碼并沒有受 synchronized 保護,于是便有可能發(fā)生以下場景:

  • 首先,消費者線程調(diào)用 take 方法并判斷 buffer.isEmpty 方法是否返回 true,若為 true 代表 buffer 是空的,則線程希望進入等待,但是在線程調(diào)用 wait 方法之前,就被調(diào)度器暫停了,所以此時還沒來得及執(zhí)行 wait 方法。

  • 此時生產(chǎn)者開始運行,執(zhí)行了整個 give 方法,它往 buffer 中添加了數(shù)據(jù),并執(zhí)行了 notify 方法,但 notify 并沒有任何效果,因為消費者線程的 wait 方法沒來得及執(zhí)行,所以沒有線程在等待被喚醒。

  • 此時,剛才被調(diào)度器暫停的消費者線程回來繼續(xù)執(zhí)行 wait 方法并進入了等待。

雖然剛才消費者判斷了 buffer.isEmpty 條件,但真正執(zhí)行 wait 方法時,之前的 buffer.isEmpty 的結(jié)果已經(jīng)過期了,不再符合最新的場景了,因為這里的“判斷-執(zhí)行”不是一個原子操作,它在中間被打斷了,是線程不安全的

假設(shè)這時沒有更多的生產(chǎn)者進行生產(chǎn),消費者便有可能陷入無窮無盡的等待,因為它錯過了剛才 give 方法內(nèi)的 notify 的喚醒。

因為 wait 方法所在的 take 方法沒有被 synchronized 保護,所以它的 while 判斷和 wait 方法無法構(gòu)成原子操作,那么此時整個程序就很容易出錯。

把代碼改寫成源碼注釋所要求的被 synchronized 保護的同步代碼塊的形式,代碼如下。

public void give(String data) {
   synchronized (this) {
      buffer.add(data);
      notify();
  }
}
 
public String take() throws InterruptedException {
   synchronized (this) {
    while (buffer.isEmpty()) {
         wait();
       }
     return buffer.remove();
  }
}

這樣就可以確保 notify 方法永遠不會在 buffer.isEmpty 和 wait 方法之間被調(diào)用,提升了程序的安全性。

wait 方法會釋放 monitor 鎖,這也要求我們必須首先進入到 synchronized 內(nèi)持有這把鎖。

這里還存在一個“虛假喚醒”(spurious wakeup)的問題,線程可能在既沒有被notify/notifyAll,也沒有被中斷或者超時的情況下被喚醒,這種喚醒是我們不希望看到的。雖然在實際生產(chǎn)中,虛假喚醒發(fā)生的概率很小,但是程序依然需要保證在發(fā)生虛假喚醒的時候的正確性,所以就需要采用while循環(huán)的結(jié)構(gòu)。

while (condition does not hold)
    obj.wait();

這樣即便被虛假喚醒了,也會再次檢查while里面的條件,如果不滿足條件,就會繼續(xù)wait,也就消除了虛假喚醒的風(fēng)險。

為什么 wait/notify/notifyAll 被定義在 Object 類中,而 sleep 定義在 Thread 類中?

主要有兩點原因:
1. 因為 Java 中每個對象都有一把稱之為 monitor 監(jiān)視器的鎖,由于每個對象都可以上鎖,這就要求在對象頭中有一個用來保存鎖信息的位置。這個鎖是對象級別的,而非線程級別的,wait/notify/notifyAll 也都是鎖級別的操作,它們的鎖屬于對象,所以把它們定義在 Object 類中是最合適,因為 Object 類是所有對象的父類。

2. 因為如果把 wait/notify/notifyAll 方法定義在 Thread 類中,會帶來很大的局限性,比如一個線程可能持有多把鎖,以便實現(xiàn)相互配合的復(fù)雜邏輯,假設(shè)此時 wait 方法定義在 Thread 類中,如何實現(xiàn)讓一個線程持有多把鎖呢?又如何明確線程等待的是哪把鎖呢?既然我們是讓當前線程去等待某個對象的鎖,自然應(yīng)該通過操作對象來實現(xiàn),而不是操作線程。

wait/notify 和 sleep 方法的異同?

相同點:
1. 它們都可以讓線程阻塞。
2. 它們都可以響應(yīng) interrupt 中斷:在等待的過程中如果收到中斷信號,都可以進行響應(yīng),并拋出 InterruptedException 異常。

不同點:

  1. wait 方法必須在 synchronized 保護的代碼中使用,而 sleep 方法并沒有這個要求。
  2. 在同步代碼中執(zhí)行 sleep 方法時,并不會釋放 monitor 鎖,但執(zhí)行 wait 方法時會主動釋放 monitor 鎖。
  3. sleep 方法中會要求必須定義一個時間,時間到期后會主動恢復(fù),而對于沒有參數(shù)的 wait 方法而言,意味著永久等待,直到被中斷或被喚醒才能恢復(fù),它并不會主動恢復(fù)。
  4. wait/notify 是 Object 類的方法,而 sleep 是 Thread 類的方法。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容