線程間的協(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 異常。
不同點:
- wait 方法必須在 synchronized 保護的代碼中使用,而 sleep 方法并沒有這個要求。
- 在同步代碼中執(zhí)行 sleep 方法時,并不會釋放 monitor 鎖,但執(zhí)行 wait 方法時會主動釋放 monitor 鎖。
- sleep 方法中會要求必須定義一個時間,時間到期后會主動恢復(fù),而對于沒有參數(shù)的 wait 方法而言,意味著永久等待,直到被中斷或被喚醒才能恢復(fù),它并不會主動恢復(fù)。
- wait/notify 是 Object 類的方法,而 sleep 是 Thread 類的方法。