摘要:在這個(gè)等待通知機(jī)制中,我們需要考慮以下四個(gè)要素。何時(shí)等待線程要求的條件不滿足就等待。是會(huì)隨機(jī)地通知等待隊(duì)列中的一個(gè)線程,而會(huì)通知等待隊(duì)列中的所有線程。
由上一篇文章你應(yīng)該已經(jīng)知道,在 破壞占用且等待條件 的時(shí)候,如果轉(zhuǎn)出賬本和轉(zhuǎn)入賬本不滿足同時(shí)在文件架上這個(gè)條件,就用死循環(huán)的方式來(lái)循環(huán)等待,核心代碼如下:
// 一次性申請(qǐng)轉(zhuǎn)出賬戶和轉(zhuǎn)入賬戶,直到成功 while(!actr.apply(this, target)) ;
如果 apply() 操作耗時(shí)非常短,而且并發(fā)沖突量也不大時(shí),這個(gè)方案還挺不錯(cuò)的,因?yàn)檫@種場(chǎng)景下,循環(huán)上幾次或者幾十次就能一次性獲取轉(zhuǎn)出賬戶和轉(zhuǎn)入賬戶了。但是如果 apply() 操作耗時(shí)長(zhǎng),或者并發(fā)沖突量大的時(shí)候,可能要循環(huán)上萬(wàn)次才能獲取到鎖,太消耗 CPU 了。
其實(shí)在這種場(chǎng)景下,最好的方案應(yīng)該是:如果線程要求的條件(轉(zhuǎn)出賬本和轉(zhuǎn)入賬本同在文件架上)不滿足,則線程阻塞自己,進(jìn)入等待狀態(tài);當(dāng)線程要求的條件(轉(zhuǎn)出賬本和轉(zhuǎn)入賬本同在文件架上)滿足后, 通知等待的線程重新執(zhí)行。其中,使用線程阻塞的方式就能避免循環(huán)等待消耗 CPU 的問(wèn)題。
下面我們就來(lái)看看 Java 語(yǔ)言是如何支持 等待 - 通知機(jī)制
這里直接給出 等待 - 通知機(jī)制 的相關(guān)步驟:
線程首先獲取互斥鎖,當(dāng)線程要求的條件不滿足時(shí),釋放互斥鎖,進(jìn)入等待狀態(tài);當(dāng)要求的條件滿足時(shí),通知其他等待的線程,重新獲取互斥鎖.
用 synchronized 實(shí)現(xiàn)等待 - 通知機(jī)制在 Java 語(yǔ)言里,等待 - 通知機(jī)制可以有多種實(shí)現(xiàn)方式,比如 Java 語(yǔ)言內(nèi)置的 synchronized 配合 wait()、notify()、notifyAll() 這三個(gè)方法就能輕松實(shí)現(xiàn)。
先用 synchronized 實(shí)現(xiàn)互斥鎖。在下面這個(gè)圖里,左邊有一個(gè)等待隊(duì)列,同一時(shí)刻,只允許一個(gè)線程進(jìn)入 synchronized 保護(hù)的臨界區(qū),當(dāng)有一個(gè)線程進(jìn)入臨界區(qū)后,其他線程就只能進(jìn)入圖中左邊的等待隊(duì)列里等待。 這個(gè)等待隊(duì)列和互斥鎖是一對(duì)一的關(guān)系,每個(gè)互斥鎖都有自己獨(dú)立的等待隊(duì)列。
wait() 操作工作原理圖
在并發(fā)程序中,當(dāng)一個(gè)線程進(jìn)入臨界區(qū)后,由于某些條件不滿足,需要進(jìn)入等待狀態(tài),Java 對(duì)象的 wait() 方法就能夠滿足這種需求。如上圖所示,當(dāng)調(diào)用 wait() 方法后,當(dāng)前線程就會(huì)被阻塞,并且進(jìn)入到右邊的等待隊(duì)列中,這個(gè)等待隊(duì)列也是互斥鎖的等待隊(duì)列。 線程在進(jìn)入等待隊(duì)列的同時(shí),會(huì)釋放持有的互斥鎖,線程釋放鎖后,其他線程就有機(jī)會(huì)獲得鎖,并進(jìn)入臨界區(qū)了。
那線程要求的條件滿足時(shí),該怎么通知這個(gè)等待的線程呢?很簡(jiǎn)單,就是 Java 對(duì)象的 notify() 和 notifyAll() 方法。我在下面這個(gè)圖里為你大致描述了這個(gè)過(guò)程,當(dāng)條件滿足時(shí)調(diào)用 notify(),會(huì)通知等待隊(duì)列(互斥鎖的等待隊(duì)列)中的線程,告訴它條件曾經(jīng)滿足過(guò)
notify() 操作工作原理圖
為什么說(shuō)是曾經(jīng)滿足過(guò)呢?因?yàn)?notify() 只能保證在通知時(shí)間點(diǎn),條件是滿足的。而被通知線程的執(zhí)行時(shí)間點(diǎn)和通知的時(shí)間點(diǎn)基本上不會(huì)重合,所以當(dāng)線程執(zhí)行的時(shí)候,很可能條件已經(jīng)不滿足了(可能會(huì)有其他線程插隊(duì))。這一點(diǎn)你需要格外注意。除此之外,還有一個(gè)需要注意的點(diǎn),被通知的線程要想重新執(zhí)行,仍然需要獲取到互斥鎖(因?yàn)樵?jīng)獲取的鎖在調(diào)用 wait() 時(shí)已經(jīng)釋放了)。
注意 wait()、notify()、notifyAll() 方法操作的等待隊(duì)列是互斥鎖的等待隊(duì)列,所以方法要使用在
鎖上,synchronized 鎖定的是 this,那么對(duì)應(yīng)的一定是 this.wait()、this.notify()、this.notifyAll();。而且 wait()、notify()、notifyAll() 這三個(gè)方法能夠被調(diào)用的前提是已經(jīng)獲取了相應(yīng)的互斥鎖,所以我們會(huì)發(fā)現(xiàn) wait()、notify()、notifyAll() 都是在 synchronized{}內(nèi)部被調(diào)用的。如果在 synchronized{}外部調(diào)用,或者鎖定的 this,而用 target.wait() 調(diào)用的話,JVM 會(huì)拋出一個(gè)運(yùn)行時(shí)異常:java.lang.IllegalMonitorStateException
等待 - 通知機(jī)制的基本原理搞清楚后,我們來(lái)看看它如何解決一次性申請(qǐng)轉(zhuǎn)出賬戶和轉(zhuǎn)入賬戶的問(wèn)題。在這個(gè)等待 - 通知機(jī)制中,我們需要考慮以下四個(gè)要素。
互斥鎖:上一篇文章我們提到 Allocator 需要是單例的,所以我們可以用 this 作為互斥鎖。
線程要求的條件:轉(zhuǎn)出賬戶和轉(zhuǎn)入賬戶都沒(méi)有被分配過(guò)。
何時(shí)等待:線程要求的條件不滿足就等待。
何時(shí)通知:當(dāng)有線程釋放賬戶時(shí)就通知。
注意下面的判斷方式
while(條件不滿足) { wait(); }
利用這種范式可以解決上面提到的條件曾經(jīng)滿足過(guò)的情況。至于為什么這么寫(xiě),后面講解 管程的時(shí)候會(huì)在詳細(xì)解釋。
來(lái)看完成后的代碼
class Allocator { private List盡量使用 notifyAll()
在上面的代碼中,我用的是 notifyAll() 來(lái)實(shí)現(xiàn)通知機(jī)制,為什么不使用 notify() 呢?這二者是有區(qū)別的。
notify() 是會(huì)隨機(jī)地通知等待隊(duì)列中的一個(gè)線程,而 notifyAll() 會(huì)通知等待隊(duì)列中的所有線程。
從感覺(jué)上來(lái)講,應(yīng)該是 notify() 更好一些,因?yàn)榧幢阃ㄖ芯€程,也只有一個(gè)線程能夠進(jìn)入臨界區(qū)。但實(shí)際上使用 notify() 也很有風(fēng)險(xiǎn),它的風(fēng)險(xiǎn)在于可能導(dǎo)致某些線程永遠(yuǎn)不會(huì)被通知到。
假設(shè)我們有資源 A、B、C、D,線程 1 申請(qǐng)到了 AB,線程 2 申請(qǐng)到了 CD,此時(shí)線程 3 申請(qǐng) AB,會(huì)進(jìn)入等待隊(duì)列(AB 分配給線程 1,線程 3 要求的條件不滿足),線程 4 申請(qǐng) CD 也會(huì)進(jìn)入等待隊(duì)列。我們?cè)偌僭O(shè)之后線程 1 歸還了資源 AB,如果使用 notify() 來(lái)通知等待隊(duì)列中的線程,有可能被通知的是線程 4,但線程 4 申請(qǐng)的是 CD,所以此時(shí)線程 4 還是會(huì)繼續(xù)等待,而真正該喚醒的線程 3 就再也沒(méi)有機(jī)會(huì)被喚醒了。
所以除非經(jīng)過(guò)深思熟慮,否則盡量使用 notifyAll()。總結(jié)
Java 語(yǔ)言的這種實(shí)現(xiàn),背后的理論模型其實(shí)是管程,后面會(huì)專門介紹管程。現(xiàn)在你只需要能夠熟練使用就可以了。
思考:wait() 方法和 sleep() 方法都能讓當(dāng)前線程掛起一段時(shí)間,那它們的區(qū)別是什么?
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/74187.html
摘要:請(qǐng)參看前一篇文章并發(fā)學(xué)習(xí)筆記一原子性可見(jiàn)性有序性問(wèn)題六等待通知機(jī)制什么是等待通知機(jī)制當(dāng)線程不滿足某個(gè)條件,則進(jìn)入等待狀態(tài)如果線程滿足要求的某個(gè)條件后,則通知等待的線程重新執(zhí)行。經(jīng)極客時(shí)間并發(fā)編程實(shí)戰(zhàn)專欄內(nèi)容學(xué)習(xí)整理 請(qǐng)參看前一篇文章:Java 并發(fā)學(xué)習(xí)筆記(一)——原子性、可見(jiàn)性、有序性問(wèn)題 六、等待—通知機(jī)制 什么是等待通知—機(jī)制?當(dāng)線程不滿足某個(gè)條件,則進(jìn)入等待狀態(tài);如果線程滿足要...
摘要:對(duì)象改變條件對(duì)象當(dāng)前線程要等待線程終止之后才能從返回。如果線程在上的操作中被中斷,通道會(huì)被關(guān)閉,線程的中斷狀態(tài)會(huì)被設(shè)置,并得到一個(gè)。清除線程的中斷狀態(tài)。非公平性鎖雖然可能造成饑餓,但極少的線程切換,保證其更大的吞吐量。 聲明:Java并發(fā)的內(nèi)容是自己閱讀《Java并發(fā)編程實(shí)戰(zhàn)》和《Java并發(fā)編程的藝術(shù)》整理來(lái)的。 showImg(https://segmentfault.com/im...
摘要:是要和配合使用的也就是和是綁定在一起的,而的實(shí)現(xiàn)原理又依賴于,自然而然作為的一個(gè)內(nèi)部類無(wú)可厚非。示意圖如下是的內(nèi)部類,因此每個(gè)能夠訪問(wèn)到提供的方法,相當(dāng)于每個(gè)都擁有所屬同步器的引用。Condition簡(jiǎn)介Object類是Java中所有類的父類, 在線程間實(shí)現(xiàn)通信的往往會(huì)應(yīng)用到Object的幾個(gè)方法: wait(),wait(long timeout),wait(long timeout, i...
摘要:線程需要避免竟態(tài),死鎖以及很多其他共享狀態(tài)的并發(fā)性問(wèn)題。用戶線程在前臺(tái),守護(hù)線程在后臺(tái)運(yùn)行,為其他前臺(tái)線程提供服務(wù)。當(dāng)所有前臺(tái)線程都退出時(shí),守護(hù)線程就會(huì)退出。線程阻塞等待獲取某個(gè)對(duì)象鎖的訪問(wèn)權(quán)限。 1、多線程介紹 多線程優(yōu)點(diǎn) 資源利用率好 程序設(shè)計(jì)簡(jiǎn)單 服務(wù)器響應(yīng)更快 多線程缺點(diǎn) 設(shè)計(jì)更復(fù)雜 上下文切換的開(kāi)銷 增加資源消耗線程需要內(nèi)存維護(hù)本地的堆棧,同時(shí)需要操作系統(tǒng)資源管理線程。...
摘要:運(yùn)行可運(yùn)行狀態(tài)的線程獲得了時(shí)間片,執(zhí)行程序代碼。阻塞的情況分三種一等待阻塞運(yùn)行的線程執(zhí)行方法,會(huì)把該線程放入等待隊(duì)列中。死亡線程方法執(zhí)行結(jié)束,或者因異常退出了方法,則該線程結(jié)束生命周期。死亡的線程不可再次復(fù)生。 系列文章傳送門: Java多線程學(xué)習(xí)(一)Java多線程入門 Java多線程學(xué)習(xí)(二)synchronized關(guān)鍵字(1) java多線程學(xué)習(xí)(二)synchronized關(guān)鍵...
閱讀 3380·2021-11-22 09:34
閱讀 650·2021-11-19 11:29
閱讀 1350·2019-08-30 15:43
閱讀 2232·2019-08-30 14:24
閱讀 1867·2019-08-29 17:31
閱讀 1223·2019-08-29 17:17
閱讀 2617·2019-08-29 15:38
閱讀 2729·2019-08-26 12:10