等待/通知機(jī)制 wait/notify/notifyAll 方法

線程間的協(xié)作

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

等待/通知機(jī)制

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

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

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

通知方遵循如下原則壶熏。
1)獲得對(duì)象的鎖句柠。
2)改變條件。
3)通知所有等待在對(duì)象上的線程棒假。

在調(diào)用 wait()溯职、notify() 系列方法之前,線程必須要獲得該對(duì)象的對(duì)象級(jí)別鎖帽哑,即只能在同步方法或同步塊中調(diào)用 wait() 方法谜酒、notify()系列方法,進(jìn)入 wait() 方法后妻枕,當(dāng)前線程釋放鎖僻族,在從 wait() 返回前,線程與其他線程競(jìng)爭重新獲得鎖屡谐,執(zhí)行 notify() 系列方法的線程退出調(diào)用了 notifyAll 的 synchronized 代碼塊的時(shí)候后述么,他們就會(huì)去競(jìng)爭。如果其中一個(gè)線程獲得了該對(duì)象鎖愕掏,它就會(huì)繼續(xù)往下執(zhí)行度秘,在它退出 synchronized 代碼塊,釋放鎖后饵撑,其他的已經(jīng)被喚醒的線程將會(huì)繼續(xù)競(jìng)爭獲取該鎖敷钾,一直進(jìn)行下去,直到所有被喚醒的線程都執(zhí)行完畢肄梨。

notify 和 notifyAll 應(yīng)該用誰
盡可能用 notifyall(),謹(jǐn)慎使用 notify()挠锥,因?yàn)?notify() 只會(huì)喚醒一個(gè)線程众羡,我們無法確保被喚醒的這個(gè)線程一定就是我們需要喚醒的線程

wait/notify/notifyAll 方法的使用注意事項(xiàng)。

我們主要從三個(gè)問題入手:

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

為什么 wait 方法必須在 synchronized 保護(hù)的同步代碼中使用齐婴?

先來看看 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 方法時(shí)柠偶,必須把 wait 方法寫在 synchronized 保護(hù)的 while 代碼塊中情妖,并始終判斷執(zhí)行條件是否滿足,如果滿足就往下繼續(xù)執(zhí)行诱担,如果不滿足就執(zhí)行 wait 方法毡证,而在執(zhí)行 wait 方法之前,必須先持有對(duì)象的 monitor 鎖蔫仙,也就是通常所說的 synchronized 鎖料睛。

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

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 方法負(fù)責(zé)往 buffer 中添加數(shù)據(jù)恤煞,添加完之后執(zhí)行 notify 方法來喚醒之前等待的線程,而 take 方法負(fù)責(zé)檢查整個(gè) buffer 是否為空施籍,如果為空就進(jìn)入等待居扒,如果不為空就取出一個(gè)數(shù)據(jù),這是典型的生產(chǎn)者消費(fèi)者的思想法梯。

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

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

  • 此時(shí)生產(chǎn)者開始運(yùn)行捂掰,執(zhí)行了整個(gè) give 方法敢会,它往 buffer 中添加了數(shù)據(jù),并執(zhí)行了 notify 方法这嚣,但 notify 并沒有任何效果鸥昏,因?yàn)橄M(fèi)者線程的 wait 方法沒來得及執(zhí)行,所以沒有線程在等待被喚醒姐帚。

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

雖然剛才消費(fèi)者判斷了 buffer.isEmpty 條件罐旗,但真正執(zhí)行 wait 方法時(shí)膳汪,之前的 buffer.isEmpty 的結(jié)果已經(jīng)過期了,不再符合最新的場(chǎng)景了九秀,因?yàn)檫@里的“判斷-執(zhí)行”不是一個(gè)原子操作遗嗽,它在中間被打斷了,是線程不安全的

假設(shè)這時(shí)沒有更多的生產(chǎn)者進(jìn)行生產(chǎn)鼓蜒,消費(fèi)者便有可能陷入無窮無盡的等待痹换,因?yàn)樗e(cuò)過了剛才 give 方法內(nèi)的 notify 的喚醒征字。

因?yàn)?wait 方法所在的 take 方法沒有被 synchronized 保護(hù),所以它的 while 判斷和 wait 方法無法構(gòu)成原子操作晴音,那么此時(shí)整個(gè)程序就很容易出錯(cuò)柔纵。

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

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 方法永遠(yuǎn)不會(huì)在 buffer.isEmpty 和 wait 方法之間被調(diào)用搁料,提升了程序的安全性。

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

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

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

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

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

主要有兩點(diǎn)原因:
1. 因?yàn)?Java 中每個(gè)對(duì)象都有一把稱之為 monitor 監(jiān)視器的鎖,由于每個(gè)對(duì)象都可以上鎖匪凉,這就要求在對(duì)象頭中有一個(gè)用來保存鎖信息的位置枪眉。這個(gè)鎖是對(duì)象級(jí)別的,而非線程級(jí)別的再层,wait/notify/notifyAll 也都是鎖級(jí)別的操作贸铜,它們的鎖屬于對(duì)象,所以把它們定義在 Object 類中是最合適树绩,因?yàn)?Object 類是所有對(duì)象的父類。

2. 因?yàn)槿绻?wait/notify/notifyAll 方法定義在 Thread 類中隐轩,會(huì)帶來很大的局限性饺饭,比如一個(gè)線程可能持有多把鎖,以便實(shí)現(xiàn)相互配合的復(fù)雜邏輯职车,假設(shè)此時(shí) wait 方法定義在 Thread 類中瘫俊,如何實(shí)現(xiàn)讓一個(gè)線程持有多把鎖呢鹊杖?又如何明確線程等待的是哪把鎖呢?既然我們是讓當(dāng)前線程去等待某個(gè)對(duì)象的鎖扛芽,自然應(yīng)該通過操作對(duì)象來實(shí)現(xiàn)骂蓖,而不是操作線程。

wait/notify 和 sleep 方法的異同川尖?

相同點(diǎn):
1. 它們都可以讓線程阻塞登下。
2. 它們都可以響應(yīng) interrupt 中斷:在等待的過程中如果收到中斷信號(hào),都可以進(jìn)行響應(yīng)叮喳,并拋出 InterruptedException 異常被芳。

不同點(diǎn):

  1. wait 方法必須在 synchronized 保護(hù)的代碼中使用,而 sleep 方法并沒有這個(gè)要求馍悟。
  2. 在同步代碼中執(zhí)行 sleep 方法時(shí)畔濒,并不會(huì)釋放 monitor 鎖,但執(zhí)行 wait 方法時(shí)會(huì)主動(dòng)釋放 monitor 鎖锣咒。
  3. sleep 方法中會(huì)要求必須定義一個(gè)時(shí)間侵状,時(shí)間到期后會(huì)主動(dòng)恢復(fù),而對(duì)于沒有參數(shù)的 wait 方法而言毅整,意味著永久等待趣兄,直到被中斷或被喚醒才能恢復(fù),它并不會(huì)主動(dòng)恢復(fù)毛嫉。
  4. wait/notify 是 Object 類的方法诽俯,而 sleep 是 Thread 類的方法。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末承粤,一起剝皮案震驚了整個(gè)濱河市暴区,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌辛臊,老刑警劉巖仙粱,帶你破解...
    沈念sama閱讀 219,188評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異彻舰,居然都是意外死亡伐割,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門刃唤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來隔心,“玉大人,你說我怎么就攤上這事尚胞∮不簦” “怎么了?”我有些...
    開封第一講書人閱讀 165,562評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵笼裳,是天一觀的道長唯卖。 經(jīng)常有香客問我粱玲,道長,這世上最難降的妖魔是什么拜轨? 我笑而不...
    開封第一講書人閱讀 58,893評(píng)論 1 295
  • 正文 為了忘掉前任抽减,我火速辦了婚禮,結(jié)果婚禮上橄碾,老公的妹妹穿的比我還像新娘卵沉。我一直安慰自己,他們只是感情好堪嫂,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,917評(píng)論 6 392
  • 文/花漫 我一把揭開白布偎箫。 她就那樣靜靜地躺著,像睡著了一般皆串。 火紅的嫁衣襯著肌膚如雪淹办。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,708評(píng)論 1 305
  • 那天恶复,我揣著相機(jī)與錄音怜森,去河邊找鬼。 笑死谤牡,一個(gè)胖子當(dāng)著我的面吹牛副硅,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播翅萤,決...
    沈念sama閱讀 40,430評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼恐疲,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了套么?” 一聲冷哼從身側(cè)響起培己,我...
    開封第一講書人閱讀 39,342評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎胚泌,沒想到半個(gè)月后省咨,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,801評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡玷室,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,976評(píng)論 3 337
  • 正文 我和宋清朗相戀三年零蓉,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片穷缤。...
    茶點(diǎn)故事閱讀 40,115評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡敌蜂,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出津肛,到底是詐尸還是另有隱情章喉,我是刑警寧澤,帶...
    沈念sama閱讀 35,804評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站囊陡,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏掀亥。R本人自食惡果不足惜撞反,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,458評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望搪花。 院中可真熱鬧遏片,春花似錦、人聲如沸撮竿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽幢踏。三九已至髓需,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間房蝉,已是汗流浹背僚匆。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評(píng)論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留搭幻,地道東北人咧擂。 一個(gè)月前我還...
    沈念sama閱讀 48,365評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像檀蹋,于是被迫代替她去往敵國和親松申。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,055評(píng)論 2 355