條件隊(duì)列大法好:使用wait柄慰、notify和notifyAll的正確姿勢(shì)

前面介紹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è)目的,但是必須保留本文的署名及鏈接腕巡。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末玄坦,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子绘沉,更是在濱河造成了極大的恐慌煎楣,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,734評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件车伞,死亡現(xiàn)場(chǎng)離奇詭異择懂,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)另玖,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門困曙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人谦去,你說我怎么就攤上這事慷丽。” “怎么了鳄哭?”我有些...
    開封第一講書人閱讀 164,133評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵要糊,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我妆丘,道長(zhǎng)锄俄,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,532評(píng)論 1 293
  • 正文 為了忘掉前任勺拣,我火速辦了婚禮奶赠,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘药有。我一直安慰自己车柠,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,585評(píng)論 6 392
  • 文/花漫 我一把揭開白布塑猖。 她就那樣靜靜地躺著竹祷,像睡著了一般。 火紅的嫁衣襯著肌膚如雪羊苟。 梳的紋絲不亂的頭發(fā)上塑陵,一...
    開封第一講書人閱讀 51,462評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音蜡励,去河邊找鬼令花。 笑死阻桅,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的兼都。 我是一名探鬼主播嫂沉,決...
    沈念sama閱讀 40,262評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼扮碧!你這毒婦竟也來了趟章?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,153評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤慎王,失蹤者是張志新(化名)和其女友劉穎蚓土,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體赖淤,經(jīng)...
    沈念sama閱讀 45,587評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蜀漆,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,792評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了咱旱。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片确丢。...
    茶點(diǎn)故事閱讀 39,919評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖吐限,靈堂內(nèi)的尸體忽然破棺而出蠕嫁,到底是詐尸還是另有隱情,我是刑警寧澤毯盈,帶...
    沈念sama閱讀 35,635評(píng)論 5 345
  • 正文 年R本政府宣布剃毒,位于F島的核電站,受9級(jí)特大地震影響搂赋,放射性物質(zhì)發(fā)生泄漏赘阀。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,237評(píng)論 3 329
  • 文/蒙蒙 一脑奠、第九天 我趴在偏房一處隱蔽的房頂上張望基公。 院中可真熱鬧,春花似錦宋欺、人聲如沸轰豆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)酸休。三九已至,卻和暖如春祷杈,著一層夾襖步出監(jiān)牢的瞬間斑司,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工但汞, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留宿刮,地道東北人互站。 一個(gè)月前我還...
    沈念sama閱讀 48,048評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像僵缺,于是被迫代替她去往敵國(guó)和親胡桃。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,864評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容