前面介紹wait和notify的基本語(yǔ)義鳍悠,參考條件隊(duì)列大法好:wait和notify的基本語(yǔ)義。這篇講講使用wait坐搔、notify藏研、notifyAll的正確姿勢(shì)愚争。
一定要先看語(yǔ)義钠乏,保證自己掌握了基本語(yǔ)義,再來學(xué)習(xí)如何使用化撕。
基本原理
狀態(tài)依賴的類
狀態(tài)依賴的類:在狀態(tài)依賴的類中凳忙,存在著某些操作业踏,它們擁有基于狀態(tài)的前提條件。也就是說涧卵,只有該狀態(tài)滿足某種前提條件時(shí)勤家,操作才會(huì)繼續(xù)執(zhí)行。
例如柳恐,要想從空隊(duì)列中取得元素伐脖,必須等待隊(duì)列的狀態(tài)變?yōu)椤胺强铡保辉谶@個(gè)前提條件得到滿足之前乐设,獲取元素的操作將保持阻塞讼庇。
如果是頭一次了解狀態(tài)依賴類的概念,很容易將狀態(tài)依賴類與并發(fā)容器混淆伤提。實(shí)際上,二者是不對(duì)等的概念:
- 并發(fā)容器的關(guān)鍵詞是“容器”认烁,其提供了不同的并發(fā)特征(包括性能肿男、安全、活躍性等方面)却嗡,用戶大多數(shù)時(shí)候可以直接使用這些容器舶沛。
- 狀態(tài)依賴的類的關(guān)鍵詞是“依賴”,其提供的是狀態(tài)同步的基本邏輯窗价,往往用于維護(hù)并發(fā)程序的狀態(tài)如庭,例如構(gòu)建并發(fā)容器等,也可以直接由用戶使用撼港。
可阻塞的狀態(tài)依賴操作
狀態(tài)依賴類的核心是狀態(tài)依賴操作坪它,最常用的是可阻塞的狀態(tài)依賴操作骤竹。
其本質(zhì)如下:
acquire lock(on object state) // 測(cè)試前需要獲取鎖,以保證測(cè)試時(shí)條件不變
while (precondition does not hold) { // pre-check防止信號(hào)丟失往毡;re-check防止過早喚醒
release lock // 如果條件尚未滿足蒙揣,就釋放鎖,允許其他線程修改條件
wait until precondition might hold, interrupted or timeout expires
acquire lock // 再次測(cè)試前需要獲取鎖开瞭,以保證測(cè)試時(shí)條件不變
}
do sth // 如果條件已滿足懒震,就執(zhí)行動(dòng)作
release lock // 最后再釋放鎖
注釋內(nèi)容可暫時(shí)不關(guān)注,后面逐項(xiàng)解釋嗤详。
對(duì)應(yīng)修改狀態(tài)的操作:
acquire lock(on object state)
do sth, to make precondition might be hold
release lock
條件隊(duì)列的核心行為就是一個(gè)可阻塞的狀態(tài)依賴操作个扰。
在條件隊(duì)列中,precondition(前置條件)是一個(gè)單元的條件謂詞葱色,也即條件隊(duì)列等待的條件(signal/notify)递宅。大部分使用條件隊(duì)列的場(chǎng)景,本質(zhì)上是在基于單元條件謂詞構(gòu)造多元條件謂詞的狀態(tài)依賴類冬筒。
正確姿勢(shì)
version1:baseline
如果將具體場(chǎng)景中的多元條件謂詞稱為“條件謂詞”恐锣,那么,構(gòu)造出來的仍然是一個(gè)可阻塞的狀態(tài)依賴操作舞痰。
可以認(rèn)為土榴,條件謂詞和條件隊(duì)列針對(duì)的都是同一個(gè)“條件”,只不過條件謂詞刻畫該“條件”的內(nèi)容响牛,條件隊(duì)列用于維護(hù)狀態(tài)依賴玷禽,即4行的“
wait until
”。
理解了這一點(diǎn)后呀打,基于條件隊(duì)列的同步將變的非常簡(jiǎn)單矢赁。大體上是使用Java提供的API實(shí)現(xiàn)可阻塞的狀態(tài)依賴操作。
key point
基本點(diǎn):
- 在等待線程中獲取條件謂詞的狀態(tài)贬丛,如果不滿足就等待撩银,滿足就繼續(xù)操作
- 在通知線程中修改條件謂詞的狀態(tài),之后發(fā)出通知
加鎖:
- 獲取豺憔、修改條件謂詞的狀態(tài)是互斥的额获,需要加鎖保護(hù)
- 滿足條件謂詞的值后,需要保證操作期間恭应,條件謂詞的狀態(tài)不變抄邀,因此,等待線程的加鎖范圍應(yīng)擴(kuò)展為從檢查條件之前開始昼榛,然后進(jìn)入等待境肾,最后到操作之后結(jié)束
- 同一時(shí)間,只能執(zhí)行一種操作,對(duì)應(yīng)條件謂詞的一次狀態(tài)轉(zhuǎn)換奥喻,因此偶宫,通知線程的加鎖范圍應(yīng)擴(kuò)展為從操作之前開始,到發(fā)出通知之后結(jié)束
API相關(guān):
- 在通知線程等待時(shí)衫嵌,通知線程需要釋放自己持有的鎖读宙,待條件謂詞滿足時(shí)重新競(jìng)爭(zhēng)鎖。因此楔绞,我們?cè)凇巴ㄖ?等待”模型中使用的鎖必須與條件隊(duì)列關(guān)聯(lián)——在Java中结闸,這一語(yǔ)義都由wait()方法完成,因此酒朵,不需要用戶顯示的釋放鎖和獲取鎖桦锄。
偽碼
使用共享對(duì)象shared中的內(nèi)置鎖與內(nèi)置條件隊(duì)列。
// 等待線程
synchronized (shared) {
if (precondition does not hold) {
shared.wait();
}
do sth;
}
// 通知線程
synchronized (shared) {
do sth, to make precondition might be hold;
shared.notify();
}
version2:過早喚醒
Java提供的條件隊(duì)列(無論是內(nèi)置條件隊(duì)列還是顯示條件隊(duì)列)本身不支持多元條件謂詞蔫耽,因此盡管我們?cè)噲D基于條件隊(duì)列內(nèi)置的單元條件謂詞構(gòu)造多元條件謂詞的狀態(tài)依賴類结耀,但實(shí)際上二者在語(yǔ)義上無法綁定在一起——這導(dǎo)致了很多問題。
仍舊以內(nèi)置條件隊(duì)列為例匙铡。它提供了內(nèi)置單元條件謂詞上的“等待”和“通知”的語(yǔ)義图甜,當(dāng)內(nèi)置單元條件謂詞滿足時(shí),等待線程被喚醒鳖眼,但該線程無法得知是否是多元條件謂詞是否也已經(jīng)滿足黑毅。不考慮惡意代碼,被喚醒通常有以下原因:
- 自己的多元條件謂詞得到滿足(這是我們最期望的情況)
- 超時(shí)(如果你不希望一直等下去的話)
- 被中斷
- 與你共用一個(gè)條件隊(duì)列的多元條件謂詞得到滿足(我們不建議這樣做钦讳,但內(nèi)置條件隊(duì)列經(jīng)常會(huì)遇到這樣的情況)
- 如果你恰好使用了一個(gè)線程對(duì)象s作為條件隊(duì)列矿瘦,那么線程死亡的時(shí)候,會(huì)自動(dòng)喚醒等待s的線程
所以愿卒,當(dāng)線程從wait()方法返回時(shí)缚去,必須再次檢查多元條件謂詞是否滿足。改起來很簡(jiǎn)單:
// 等待線程
synchronized (shared) {
while (precondition does not hold) {
shared.wait();
}
do sth;
}
另一方面琼开,就算這次被喚醒是因?yàn)槎嘣獥l件謂詞得到滿足易结,仍然需要再次檢查。別忘了柜候,wait()方法完成了“釋放鎖->等待通知->收到通知->競(jìng)爭(zhēng)鎖->重新獲取鎖”一系列事件搞动,雖然“收到通知”時(shí)多元條件謂詞已經(jīng)得到滿足,但從“收到通知”到“重新獲取鎖”之間改橘,可能有其他線程已經(jīng)獲取了這個(gè)鎖滋尉,并修改了多元條件謂詞的狀態(tài)玉控,使得多元條件謂詞再次變得不滿足飞主。
以上幾種情況即為“過早喚醒
”。
version3:信號(hào)丟失
還有一個(gè)很難注意到的問題:re-check時(shí),使用while-do還是do-while碌识?
本質(zhì)上是一個(gè)”先檢查還是先wait“的問題碾篡,發(fā)生在等待線程和通知線程啟動(dòng)的過程中。假設(shè)使用do-while:如果通知線程先發(fā)出通知筏餐,等待線程再進(jìn)入等待开泽,那么等待線程將永遠(yuǎn)不會(huì)醒來,也就是“信號(hào)丟失
”魁瞪。這是因?yàn)槟侣桑瑮l件隊(duì)列的通知沒有“粘附性
”:如果條件隊(duì)列收到通知時(shí),沒有線程等待导俘,通知就被丟棄了峦耘。
要解決信號(hào)丟失問題,必須“先檢查再wait”旅薄,使用while-do即可辅髓。
version4:信號(hào)劫持
明確了過早喚醒和信號(hào)丟失的問題,再來講信號(hào)劫持就容易多了少梁。
信號(hào)劫持發(fā)生在使用notify()時(shí)洛口,notifyAll()不會(huì)出現(xiàn)該問題。
假設(shè)等待線程T1凯沪、T2的條件謂詞不同第焰,但共用一個(gè)條件隊(duì)列s。此時(shí)著洼,T2的條件謂詞得到滿足樟遣,s收到通知,隨機(jī)從等待在s上的T1身笤、T2中選擇了T1豹悬。T1的條件謂詞還未滿足,經(jīng)過re-check后再次進(jìn)入了阻塞狀態(tài)液荸;而條件謂詞已經(jīng)滿足的T2卻沒有被喚醒瞻佛。由于T1的過早喚醒,使得T2的信號(hào)丟失了娇钱,我們就說在T2上發(fā)生了信號(hào)劫持伤柄。
將通知線程代碼中的notify()替換為notifyAll()可以解決信號(hào)劫持的問題:
// 通知線程
synchronized (shared) {
do sth, to make precondition might be hold;
shared.notifyAll();
}
不過,notifyAll()的副作用非常大:一次性喚醒等待在條件隊(duì)列上的所有線程文搂,除了最終競(jìng)爭(zhēng)到鎖的線程适刀,其他線程都相當(dāng)于無效競(jìng)爭(zhēng)。事實(shí)上煤蹭,使用notify()也可以笔喉,只需要保證每次都能叫醒正確的等待線程取视。方法很簡(jiǎn)單:
-
一個(gè)條件隊(duì)列只與一個(gè)多元條件謂詞綁定,即“
單進(jìn)單出
”常挚。
如果使用內(nèi)置條件隊(duì)列作谭,由于一個(gè)內(nèi)置鎖只關(guān)聯(lián)了一個(gè)內(nèi)置條件隊(duì)列,單進(jìn)單出的條件將很難滿足(如隊(duì)列非空與隊(duì)列非滿)奄毡。顯式鎖(如ReentrantLock)提供了Lock#newCondition()方法折欠,能在一個(gè)顯式鎖上創(chuàng)建多個(gè)顯示條件隊(duì)列,能保證滿足該條件吼过。
總之锐秦,信號(hào)劫持問題需要在設(shè)計(jì)狀態(tài)依賴類的時(shí)候解決。如果可以避免信號(hào)劫持盗忱,還是要使用notify():
// 通知線程
synchronized (shared) {
do sth, to make precondition might be hold;
shared.notify();
}
final version
大體框架記住后农猬,使用條件隊(duì)列的正確姿勢(shì)可以精簡(jiǎn)為以下幾個(gè)要點(diǎn):
- 全程加鎖
- while-do 等待
- 要想使用notify,必須保證單進(jìn)單出
最后給一個(gè)之前手?jǐn)]的生產(chǎn)者消費(fèi)者模型售淡,明確使用wait斤葱、notify、notifyAll的正確姿勢(shì)揖闸,詳細(xì)參考Java實(shí)現(xiàn)生產(chǎn)者-消費(fèi)者模型揍堕。
該例中,生產(chǎn)者與消費(fèi)者互為等待線程與通知線程汤纸;兩個(gè)條件謂詞非空buffer.size() > 0
與非滿buffer.size() < cap
共用同一個(gè)條件隊(duì)列BUFFER_LOCK
衩茸,需要使用notifyAll避免信號(hào)劫持。簡(jiǎn)化如下:
public class WaitNotifyModel implements Model {
private final Object BUFFER_LOCK = new Object();
private final Queue<Task> buffer = new LinkedList<>();
...
private class ConsumerImpl extends AbstractConsumer implements Consumer, Runnable {
@Override
public void consume() throws InterruptedException {
synchronized (BUFFER_LOCK) {
while (buffer.size() == 0) {
BUFFER_LOCK.wait();
}
Task task = buffer.poll();
assert task != null;
// 固定時(shí)間范圍的消費(fèi)贮泞,模擬相對(duì)穩(wěn)定的服務(wù)器處理過程
Thread.sleep(500 + (long) (Math.random() * 500));
System.out.println("consume: " + task.no);
BUFFER_LOCK.notifyAll();
}
}
}
private class ProducerImpl extends AbstractProducer implements Producer, Runnable {
@Override
public void produce() throws InterruptedException {
// 不定期生產(chǎn)楞慈,模擬隨機(jī)的用戶請(qǐng)求
Thread.sleep((long) (Math.random() * 1000));
synchronized (BUFFER_LOCK) {
while (buffer.size() == cap) {
BUFFER_LOCK.wait();
}
Task task = new Task(increTaskNo.getAndIncrement());
buffer.offer(task);
System.out.println("produce: " + task.no);
BUFFER_LOCK.notifyAll();
}
}
}
...
}
建議感興趣的讀者繼續(xù)閱讀源碼|并發(fā)一枝花之BlockingQueue,從LinkedBlockingQueue的實(shí)現(xiàn)中啃擦,學(xué)習(xí)如何保證“一個(gè)條件隊(duì)列只與一個(gè)多元條件謂詞綁定”以避免信號(hào)劫持囊蓝,還能了解到"單次通知
"、"條件通知
" 等常見優(yōu)化手段令蛉。
總結(jié)
條件隊(duì)列的使用是并發(fā)面試中的一個(gè)好考點(diǎn)聚霜。猴子第一次遇到時(shí)一臉懵逼,嘰里咕嚕也沒有答上來珠叔,現(xiàn)在寫文章時(shí)才發(fā)現(xiàn)自己根本沒有理解蝎宇。如果本文有哪里說錯(cuò)了,希望您能通過簡(jiǎn)書或郵箱聯(lián)系我祷安,提前致謝姥芥。
挖坑系列——以后講一下wait、notify汇鞭、notifyAll的實(shí)現(xiàn)機(jī)制凉唐。
本文鏈接:條件隊(duì)列大法好:使用wait报嵌、notify和notifyAll的正確姿勢(shì)
作者:猴子007
出處:https://monkeysayhi.github.io
本文基于 知識(shí)共享署名-相同方式共享 4.0 國(guó)際許可協(xié)議發(fā)布,歡迎轉(zhuǎn)載熊榛,演繹或用于商業(yè)目的,但是必須保留本文的署名及鏈接腕巡。