Java AbstractQueuedSynchronizer源碼閱讀2-addWaiter()

AbstractQueuedSynchronizer既然是同步器實(shí)現(xiàn)框架蕊温,關(guān)鍵便在于處理好多線程運(yùn)行時(shí)的問(wèn)題搬设。通過(guò)Java AbstractQueuedSynchronizer源碼閱讀1-基于隊(duì)列的同步器框架,可以了解到addWaiter()的功能是將Node入隊(duì)排拷,那么addWaiter()是如何保證多線程運(yùn)行下入隊(duì)操作的正確性的呢?

addWaiter()使用了原子性方法compareAndSetTail()。為方便敘述倔幼,將addWater()的代碼粘貼如下:

private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);//新建與一個(gè)當(dāng)前線程關(guān)聯(lián)的node
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {//如果tail不為空
            node.prev = pred;//將新建的node加入到隊(duì)尾
            if (compareAndSetTail(pred, node)) {//調(diào)用CAS(CompareAndSet)重新設(shè)置tail
                pred.next = node;
                return node;
            }
        }
        //如果入隊(duì)失敗了,則調(diào)用enq()
        enq(node);
        return node爽待;
    }

compareAndSetTail(pred, node)會(huì)比較pred和tail是否指向同一個(gè)節(jié)點(diǎn)损同,如果是,才將tail更新為node鸟款。為何不是直接賦值膏燃,而要多做一步比較操作呢?那是因?yàn)殡m然當(dāng)前線程在聲明pred時(shí)何什,為pred賦值了tail组哩,但tail可能會(huì)被其他線程改變,而當(dāng)前線程的本地變量pred是不會(huì)感知到這個(gè)改變的处渣。

這里伶贰,通過(guò)模擬多線程執(zhí)行來(lái)加深這個(gè)理解。

假設(shè)隊(duì)列的初始狀態(tài)如下罐栈,只有一個(gè)Node(稱為Node0)黍衙,隊(duì)列的head和tail都指向Node0。


隊(duì)列的初始狀態(tài)

現(xiàn)在有線程A和線程B同時(shí)調(diào)用addWaiter()悠瞬,要向該隊(duì)列插入新的Node们豌。假設(shè)線程A和線程B的執(zhí)行流程如下圖所示(省略了部分代碼,并對(duì)代碼采取了簡(jiǎn)寫(xiě)):


線程A和線程B的執(zhí)行流程

第1步:線程A新建了Node(稱為NodeA)浅妆,并開(kāi)始入隊(duì)望迎。但是,線程A僅來(lái)的及將NodeA的prev指針賦值為tail凌外,便被調(diào)度出處理器辩尊。此時(shí)隊(duì)列的狀態(tài)如下圖所示,NodeA的prev指向了Node0康辑。圖中還另外標(biāo)注了線程A的局部變量pred摄欲,也是指向Node0的。


第1步

第2步:緊接著疮薇,線程B開(kāi)始占用處理器胸墙,執(zhí)行第2步。線程2也新建了一個(gè)Node(稱為NodeB)按咒,并且線程B成功的執(zhí)行完addWaiter()迟隅,將NodeB入隊(duì)。此時(shí)隊(duì)列的狀態(tài)如下圖所示,NodeB的prev也指向了Node0智袭。此處關(guān)鍵要注意的是奔缠,tail此時(shí)已經(jīng)指向了NodeB,但是線程A的pred依然指向Node0吼野。


第2步

第3步:線程B在入隊(duì)完成后校哎,線程A又開(kāi)始占用處理器執(zhí)行。線程A調(diào)用compareAndSetTail(pred, tail)瞳步,發(fā)現(xiàn)pred和tail并不是指向同一個(gè)Node的闷哆,該方法會(huì)返回false,線程A嘗試快速入隊(duì)失敗谚攒。之后阳准,線程A會(huì)調(diào)用enq(),重新獲取tail馏臭,并不停嘗試入隊(duì)直到成功。這里不再展開(kāi)enq()方法讼稚。最終隊(duì)列的狀態(tài)會(huì)如下所示括儒,線程B因?yàn)橄扔诰€程A成功調(diào)用了compareAndSetTail(),而位于A的前面锐想。


隊(duì)列最終狀態(tài)

如果使用的不是CAS方法帮寻,而是直接采用賦值的方式(即將compareAndSetTail(pred, node)換成tail=node),則在第3步時(shí)赠摇,會(huì)得到下面這個(gè)錯(cuò)誤的結(jié)果固逗。


錯(cuò)誤的入隊(duì)

所以說(shuō),入隊(duì)的同步關(guān)鍵在于原子性的compareAndSetTail()方法藕帜。它保證了每個(gè)線程能夠完整的執(zhí)行下面兩個(gè)操作:

  1. 設(shè)置prev烫罩,將自己鏈接到隊(duì)尾;
  2. 將tail更新為自己洽故。

這使得隊(duì)列中的tail和prev指針總是可靠的贝攒,用戶在任何時(shí)候都可以使用tail和prev去訪問(wèn)隊(duì)列。


提出一些問(wèn)題

為何不將如下入隊(duì)的兩步關(guān)鍵性操作封裝為原子性操作

  1. 設(shè)置prev时甚,將自己鏈接到隊(duì)尾隘弊;
  2. 將tail更新為自己。

如果將這兩步封裝為原子性操作荒适,那么正確的入隊(duì)就可以一次性完成梨熙。而原本的實(shí)現(xiàn)中,CAS失敗后刀诬,還需要再重試咽扇。但是,AbstractQueuedSynchronizer本身是要實(shí)現(xiàn)原子性的操作,而其本身又依賴原子性的操作肌割,感覺(jué)有點(diǎn)像是先有雞還是先有蛋的問(wèn)題了卧蜓。
其實(shí),CAS的原子性是依賴機(jī)器指令實(shí)現(xiàn)的把敞,但是機(jī)器指令無(wú)法支持以上兩步執(zhí)行的原子性弥奸。AbstractQueuedSynchronizer采取的方法是,依賴原子性的CAS以及循環(huán)奋早,來(lái)實(shí)現(xiàn)上述兩步的原子性盛霎。

為何addWaiter()實(shí)現(xiàn)時(shí),是按照下面這個(gè)順序

  1. 設(shè)置prev指針耽装;
  2. compareAndSetTail()愤炸;
  3. 設(shè)置next指針;

而不是3-2-1或是1-3-2這種順序呢掉奄?

對(duì)于3-2-1规个,我認(rèn)為,其實(shí)和1-2-3的本質(zhì)上是一樣的姓建,只是代碼實(shí)現(xiàn)時(shí)诞仓,選擇prev指針而已。1-2-3這種方式保證了prev的可靠性速兔,可以看到AbstractQueuedSynchronizer中一些需要遍歷隊(duì)列的方法墅拭,如getQueuedThreads(),都是使用的prev涣狗。

對(duì)于1-3-2谍婉,我們可以按照上面的圖"線程A和線程B的執(zhí)行流程“,再推演一遍镀钓,可以發(fā)現(xiàn)穗熬,線程A在快速入隊(duì)時(shí),對(duì)NodeA的prev指針和next指針的設(shè)置都浪費(fèi)了掸宛。而1-2-3的順序下死陆,僅是浪費(fèi)了設(shè)置prev指針這一步。

總而言之唧瘾,addWaiter()的實(shí)現(xiàn)保證了prev指針的可靠性措译。

那么,next指針既然不可靠饰序,那為何還需要呢领虹?prev指針不是已經(jīng)保證了隊(duì)列的可訪問(wèn)性了么?引用源碼中對(duì)Node的注釋求豫。

We also use "next" links to implement blocking mechanics. The thread id for each node is kept in its own node, so a predecessor signals the next node to wake up by traversing next link to determine which thread it is. Determination of successor must avoid races with newly queued nodes to set the "next" fields of their predecessors. This is solved when necessary by checking backwards from the atomically updated "tail" when a node's successor appears to be null. (Or, said differently, the next-links are an optimization so that we don't usually need a backward scan.)

這段話的大意是說(shuō)某個(gè)節(jié)點(diǎn)在釋放鎖時(shí)塌衰,需要喚醒其后繼節(jié)點(diǎn)羔杨。如果沒(méi)有next指針幢尚,那么每次都需要從tail往前遍歷跳座,next指針則優(yōu)化了這一操作珍德。但是要注意的是,因?yàn)閚ext指針并不可靠努酸,所以有時(shí)候next指針會(huì)是null服爷,此時(shí),依然需要依賴tail指針向前回溯获诈,以找到期望的節(jié)點(diǎn)仍源。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市舔涎,隨后出現(xiàn)的幾起案子笼踩,更是在濱河造成了極大的恐慌,老刑警劉巖亡嫌,帶你破解...
    沈念sama閱讀 219,427評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嚎于,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡昼伴,警方通過(guò)查閱死者的電腦和手機(jī)匾旭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)圃郊,“玉大人,你說(shuō)我怎么就攤上這事女蜈〕钟撸” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,747評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵伪窖,是天一觀的道長(zhǎng)逸寓。 經(jīng)常有香客問(wèn)我,道長(zhǎng)覆山,這世上最難降的妖魔是什么竹伸? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,939評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮簇宽,結(jié)果婚禮上勋篓,老公的妹妹穿的比我還像新娘。我一直安慰自己魏割,他們只是感情好譬嚣,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著钞它,像睡著了一般拜银。 火紅的嫁衣襯著肌膚如雪殊鞭。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,737評(píng)論 1 305
  • 那天尼桶,我揣著相機(jī)與錄音操灿,去河邊找鬼。 笑死泵督,一個(gè)胖子當(dāng)著我的面吹牛趾盐,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播幌蚊,決...
    沈念sama閱讀 40,448評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼谤碳,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了溢豆?” 一聲冷哼從身側(cè)響起蜒简,我...
    開(kāi)封第一講書(shū)人閱讀 39,352評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎漩仙,沒(méi)想到半個(gè)月后搓茬,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,834評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡队他,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評(píng)論 3 338
  • 正文 我和宋清朗相戀三年卷仑,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片麸折。...
    茶點(diǎn)故事閱讀 40,133評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡锡凝,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出垢啼,到底是詐尸還是另有隱情窜锯,我是刑警寧澤,帶...
    沈念sama閱讀 35,815評(píng)論 5 346
  • 正文 年R本政府宣布芭析,位于F島的核電站锚扎,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏馁启。R本人自食惡果不足惜驾孔,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望惯疙。 院中可真熱鬧翠勉,春花似錦、人聲如沸螟碎。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,022評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)掉分。三九已至俭缓,卻和暖如春克伊,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背华坦。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,147評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工愿吹, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人惜姐。 一個(gè)月前我還...
    沈念sama閱讀 48,398評(píng)論 3 373
  • 正文 我出身青樓犁跪,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親歹袁。 傳聞我的和親對(duì)象是個(gè)殘疾皇子坷衍,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評(píng)論 2 355

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