我試圖通過這篇文章告訴你,這行源碼有多牛逼.
你好呀,我是歪歪。
這次給你盤一個(gè)特別有意思的源碼,正如我標(biāo)題說的那樣:看懂這行源碼之后,我不禁鼓起掌來,直呼祖師爺牛逼。
這行源碼是這樣的:
java.util.concurrent.LinkedBlockingQueue#dequeue

h.next = h,不過是一個(gè)把下一個(gè)節(jié)點(diǎn)指向自己的動(dòng)作而已。
這行代碼后面的注釋“help GC”其實(shí)在 JDK 的源碼里面也隨處可見。
不管怎么看都是一行平平無奇的代碼和隨處可見的注釋而已。
但是這行代碼背后隱藏的故事,可就太有意思了,真的牛逼,兒豁嘛。

它在干啥。
首先,我們得先知道這行代碼所在的方法是在干啥,然后再去分析這行代碼的作用。
所以老規(guī)矩,先搞個(gè) Demo 出來跑跑:

在 LinkedBlockingQueue 的 remove 方法中就調(diào)用了 dequeue 方法,調(diào)用鏈路是這樣的:

這個(gè)方法在 remove 的過程中承擔(dān)一個(gè)什么樣的角色呢?
這個(gè)問題的答案可以在方法的注釋上找到:

這個(gè)方法就是從隊(duì)列的頭部,刪除一個(gè)節(jié)點(diǎn),其他啥也不干。
就拿 Demo 來說,在執(zhí)行這個(gè)方法之前,我們先看一下當(dāng)前這個(gè)鏈表的情況是怎么樣的:

這是一個(gè)單向鏈表,然后 head 結(jié)點(diǎn)里面沒有元素,即 item=null,對(duì)應(yīng)做個(gè)圖出來就是這樣的:

當(dāng)執(zhí)行完這個(gè)方法之后,鏈表變成了這樣:

再對(duì)應(yīng)做個(gè)圖出來,就是這樣的:

可以發(fā)現(xiàn) 1 沒了,因?yàn)樗钦嬲摹邦^節(jié)點(diǎn)”,所以被 remove 掉了。
這個(gè)方法就干了這么一個(gè)事兒。
雖然它一共也只有六行代碼,但是為了讓你更好的入戲,我決定先給你逐行講解一下這個(gè)方法的代碼,講著講著,你就會(huì)發(fā)現(xiàn),誒,問題它就來了。

首先,我們回到方法入口處,也就是回到這個(gè)時(shí)候:

前兩行方法是這樣的:

對(duì)應(yīng)到圖上,也就是這樣的:
- h 對(duì)應(yīng)的是 head 節(jié)點(diǎn)
- first 對(duì)應(yīng)的是 “1” 節(jié)點(diǎn)

然后,來到第三行:

h 的 next 還是 h,這就是一個(gè)自己指向自己的動(dòng)作,對(duì)應(yīng)到圖上是這樣的:

然后,第四行代碼:

把 first 變成 head:

最后,第五行和第六行:

拿到 first 的 item 值,作為方法的返回值。然后再把 first 的 item 值設(shè)置為 null。
對(duì)應(yīng)到圖中就是這樣,第五行的 x 就是 1,第六行執(zhí)行完成之后,圖就變成了這樣:

整個(gè)鏈表就變成了這樣:

那么現(xiàn)在問題來了:
如果我們沒有 h.next=h 這一行代碼,會(huì)出現(xiàn)什么問題呢?
我也不知道,但是我們可以推演一下:

也就是最終我們得到的是這樣的一個(gè)鏈表:

這個(gè)時(shí)候我們發(fā)現(xiàn),由于 head 指針的位置已經(jīng)發(fā)生了變化,而且這個(gè)鏈表又是一個(gè)單向鏈表,所以當(dāng)我們使用這個(gè)鏈表的時(shí)候,沒有任何問題。
而這個(gè)對(duì)象:

已經(jīng)沒有任何指針指向它了,那么它不經(jīng)過任何處理,也是可以被 GC 回收掉的。
對(duì)嗎?
你細(xì)細(xì)的品一品,是不是這個(gè)道理,從 GC 的角度來說它確實(shí)是“不可達(dá)了”,確實(shí)可以被回收掉了。

所以,當(dāng)時(shí)有人問了我這樣的一個(gè)問題:

我經(jīng)過上面的一頓分析,發(fā)現(xiàn):嗯,確實(shí)是這樣的,確實(shí)沒啥卵用啊,不寫這一行代碼,功能也是完成正常的。
但是當(dāng)時(shí)我是這樣回復(fù)的:

我沒有把話說滿,因?yàn)檫@一行故意寫了一行“help GC”的注釋,可能有 GC 方面的考慮。
那么到底有沒有 GC 方面的考慮,是怎么考慮的呢?
憑借著我這幾年寫文章的敏銳嗅覺,我覺得這里“大有文章”,于是我?guī)е@個(gè)問題,在網(wǎng)上溜達(dá)了一圈,還真有收獲。
help GC?
首先,一頓搜索,排除了無數(shù)個(gè)無關(guān)的線索之后,我在 openjdk 的 bug 列表里面定位到了這樣的一個(gè)鏈接:
https://bugs.openjdk.org/browse/JDK-6805775

點(diǎn)擊進(jìn)這個(gè)鏈接的原因是標(biāo)題當(dāng)時(shí)就把吸引到了,翻譯過來就是說:LinkedBlockingQueue 的節(jié)點(diǎn)應(yīng)該在成為“垃圾”之前解除自己的鏈接。
先不管啥意思吧,反正 LinkedBlockingQueue、Nodes、unlink、garbage 這些關(guān)鍵詞是完全對(duì)上了。
于是我看了一下描述部分,主要關(guān)心到了這兩個(gè)部分:

看到標(biāo)號(hào)為 ① 的地方,我才發(fā)現(xiàn)在 JDK 6 里面對(duì)應(yīng)實(shí)現(xiàn)是這樣的:

而且當(dāng)時(shí)的方法還是叫 extract 而不是 dequeue。
這個(gè)方法名稱的變化,也算是一處小細(xì)節(jié)吧。

dequeue 是一個(gè)更加專業(yè)的叫法:

仔細(xì)看 JDK 6 中的 extract 方法,你會(huì)發(fā)現(xiàn),根本就沒有 help GC 這樣的注釋,也沒有相關(guān)的代碼。
它的實(shí)現(xiàn)方式就是我前面畫圖的這種:

也就是說這行代碼一定是出于某種原因,在后面的 JDK 版本中加上的。那么為什么要進(jìn)行標(biāo)號(hào)為 ① 處那樣的修改呢?
標(biāo)號(hào)為 ② 的地方給到了一個(gè)鏈接,說是這個(gè)鏈接里面有關(guān)于這個(gè)問題深入的討論。
For details and in-depth discussion, see:
http://thread.gmane.org/gmane.comp.java.jsr.166-concurrency/5758
我非常確信我找對(duì)了地方,而且我要尋找的答案就在這個(gè)鏈接里面。
但是當(dāng)我點(diǎn)過去的時(shí)候,我發(fā)現(xiàn)不管怎么訪問,這個(gè)鏈接訪問不到了...
雖然這里的線索斷了,但是順藤摸瓜,我找到了這個(gè) BUG 鏈接:
https://bugs.openjdk.org/browse/JDK-6806875

這兩個(gè) BUG 鏈接說的其實(shí)是同一個(gè)事情,但是這個(gè)鏈接里面給了一個(gè)示例代碼。
這個(gè)代碼比較長,我給你截個(gè)圖,你先不用細(xì)看,只是對(duì)比我框起來的兩個(gè)部分,你會(huì)發(fā)現(xiàn)這兩部分的代碼其實(shí)是一樣的:

當(dāng) LinkedBlockingQueue 里面加入了 h.next=null 的代碼,跑上面的程序,輸出結(jié)果是這樣:

但是,當(dāng) LinkedBlockingQueue 使用 JDK 6 的源碼跑,也就是沒有 h.next=null 的代碼跑上面的程序,輸出結(jié)果是這樣:

產(chǎn)生了 47 次 FGC。
這個(gè)代碼,在我的電腦上跑,我用的是 JDK 8 的源碼,然后注釋掉 h.next = h 這行代碼,只是會(huì)觸發(fā)一次 FGC,時(shí)間差距是 2 倍:

加上 h.next = h,兩次時(shí)間就相對(duì)穩(wěn)定:

好,到這里,不管原理是什么,我們至少驗(yàn)證了,在這個(gè)地方必須要 help GC 一下,不然確實(shí)會(huì)有性能影響。
但是,到底是為什么呢?

在反復(fù)仔細(xì)的閱讀了這個(gè) BUG 的描述部分之后,我大概懂了。
最關(guān)鍵的一個(gè)點(diǎn)其實(shí)是藏在了前面示例代碼中我標(biāo)注了五角星的那一行注釋:
SAME test, but create the queue before GC, head node will be in old gen(頭節(jié)點(diǎn)會(huì)進(jìn)入老年代)
我大概知道問題的原因是因?yàn)椤癶ead node will be in old gen”,但是具體讓我描述出來我也有點(diǎn)說不出來。
說人話就是:我懂一點(diǎn),但是不多。
于是又經(jīng)過一番查找,我找到了這個(gè)鏈接,在這里面徹底搞明白是怎么一回事了:
http://concurrencyfreaks.blogspot.com/2016/10/self-linking-and-latency-life-of.html
在這個(gè)鏈接里面提到了一個(gè)視頻,它讓我從第 23 分鐘開始看:

我看了一下這個(gè)視頻,應(yīng)該是 2015 年發(fā)布的。因?yàn)檎麄€(gè)會(huì)議的主題是:20 years of Java, just the beginning:
https://www.infoq.com/presentations/twitter-services/

這個(gè)視頻的主題是叫做“Life if a twitter JVM engineer”,是一個(gè) twitter 的 JVM 工程師在大會(huì)分享的在工作遇到的一些關(guān)于 JVM 的問題。
雖然是全程英文,但是你知道的,我的 English level 還是比較 high 的。
日常聽說,問題不大。所以大概也就聽了個(gè)幾十遍吧,結(jié)合著他的 PPT 也就知道關(guān)于這個(gè)部分他到底在分享啥了。
我要尋找的答案,也藏在這個(gè)視頻里面。
我挑關(guān)鍵的給你說。
首先他展示了這樣的這個(gè)圖片:

老年代的 x 對(duì)象指向了年輕代的 y 對(duì)象。一個(gè)非常簡單的示意圖,他主要是想要表達(dá)“跨代引用”這個(gè)問題。
然后,出現(xiàn)了這個(gè)圖片:

這里的 Queue 就是本文中討論的 LinkedBlockingQueue。
首先可以看到整個(gè) Queue 在老年代,作為一個(gè)隊(duì)列對(duì)象,極有可能生命周期比較長,所以隊(duì)列在老年代是一個(gè)正常的現(xiàn)象。
然后我們往這個(gè)隊(duì)列里面插入了 A,B 兩個(gè)元素,由于這兩個(gè)元素是我們剛剛插入的,所以它們?cè)谀贻p代,也沒有任何毛病。
此時(shí)就出現(xiàn)了老年代的 Queue 對(duì)象,指向了位于年輕代的 A,B 節(jié)點(diǎn),這樣的跨代引用。
接著,A 節(jié)點(diǎn)被干掉了,出隊(duì):

A 出隊(duì)的時(shí)候,由于它是在年輕代的,且沒有任何老年代的對(duì)象指向它,所以它是可以被 GC 回收掉的。
同理,我們插入 D,E 節(jié)點(diǎn),并讓 B 節(jié)點(diǎn)出隊(duì):

假設(shè)此時(shí)發(fā)生一次 YGC, A,B 節(jié)點(diǎn)由于“不可達(dá)”被干掉了,C 節(jié)點(diǎn)在經(jīng)歷幾次 YGC 之后,由于不是“垃圾”,所以晉升到了老年代:

這個(gè)時(shí)候假設(shè) C 出隊(duì),你說會(huì)出現(xiàn)什么情況?

首先,我問你:這個(gè)時(shí)候 C 出隊(duì)之后,它是否是垃圾?
肯定是的,因?yàn)樗豢蛇_(dá)了嘛。從圖片上也可以看到,C 雖然在老年代,但是沒有任何對(duì)象指向它了,它確實(shí)完?duì)僮恿耍?/p>

好,接下來,請(qǐng)坐好,認(rèn)真聽了。
此時(shí),我們加入一個(gè) F 節(jié)點(diǎn),沒有任何毛?。?/p>

接著 D 元素被出隊(duì)了:

就像下面這個(gè)動(dòng)圖一樣:

我把這一幀拿出來,針對(duì)這個(gè) D 節(jié)點(diǎn),單獨(dú)的說:

假設(shè)在這個(gè)時(shí)候,再次發(fā)生 YGC,D 節(jié)點(diǎn)雖然出隊(duì)了,它也位于年輕代。但是位于老年代的 C 節(jié)點(diǎn)還指向它,所以在 YGC 的時(shí)候,垃圾回收線程不敢動(dòng)它。
因此,在幾輪 YGC 之后,本來是“垃圾”的 D,搖身一變,進(jìn)入老年代了:

雖然它依然是“垃圾”,但是它進(jìn)入了老年代,YGC 對(duì)它束手無策,得 FGC 才能干掉它了。
然后越來越多的出隊(duì)節(jié)點(diǎn),變成了這樣:

然后,他們都進(jìn)入了老年代:

我們站在上帝視角,我們知道,這一串節(jié)點(diǎn),應(yīng)該在 YGC 的時(shí)候就被回收掉。
但是這種情況,你讓 GC 怎么處理?
它根本就處理不了。
GC 線程沒有上帝視角,站在它的視角,它做的每一步動(dòng)作都是正確的、符合規(guī)定的。最終呈現(xiàn)的效果就是必須要經(jīng)歷 FGC 才能把這些本來早就應(yīng)該回收的節(jié)點(diǎn),進(jìn)行回收。而我們知道,F(xiàn)GC 是應(yīng)該盡量避免的,所以這個(gè)處置方案,還是“差點(diǎn)意思”的。
所以,我們應(yīng)該怎么辦?
你回想一下,萬惡之源,是不是這個(gè)時(shí)候:

C 雖然被移出隊(duì)列了,但是它還持有一個(gè)下一個(gè)節(jié)點(diǎn)的引用,讓這個(gè)引用變成跨代引用的時(shí)候,就出毛病了。
所以,help GC,這不就來了嗎?

不管你是位于年輕代還是老年代,只要是出隊(duì),就把你的 next 引用干掉,杜絕出現(xiàn)前面我們分析的這種情況。
這個(gè)時(shí)候,你再回過頭去看前面提到的這句話:
head node will be in old gen...
你就應(yīng)該懂得起,為什么 head node 在 old gen 就要出事兒。
h.next=null ???
前面一節(jié),經(jīng)過一頓分析之后,知道了為什么要有這一行代碼:

但是你仔細(xì)一看,在我們的源碼里面是 h.hext=h 呀?
而且,經(jīng)過前面的分析我們可以知道,理論上,h.next=null 和 h.hext=h 都能達(dá)到 help GC 的目的,那么為什么最終的寫法是 h.hext=h 呢?
或者換句話說:為什么是 h.next=h,而不是 h.next=null 呢?
針對(duì)這個(gè)問題,我也盯著源碼,仔細(xì)思考了很久,最終得出了一個(gè)“非常大膽”的結(jié)論是:這兩個(gè)寫法是一樣的,不過是編碼習(xí)慣不一樣而已。
但是,注意,我要說但是了。
再次經(jīng)過一番查詢、分析和論證,這個(gè)地方它還必須得是 h.next=h。
因?yàn)樵谶@個(gè) bug 下面有這樣的一句討論:

關(guān)鍵詞是:weakly consistent iterator,弱一致性迭代器。也就是說這個(gè)問題的答案是藏在 iterator 迭代器里面的。
在 iterator 對(duì)應(yīng)的源碼中,有這樣的一個(gè)方法:
java.util.concurrent.LinkedBlockingQueue.Itr#nextNode

針對(duì) if 判斷中的 s==p,我們把 s 替換一下,就變成了 p.next=p:

那么什么時(shí)候會(huì)出現(xiàn) p.next=p 這樣的代碼呢?
答案就藏在這個(gè)方法的注釋部分:dequeued nodes (p.next == p)

dequeue 這不是巧了嗎,這不是和前面給呼應(yīng)起來了嗎?
好,到這里,我要開始給你畫圖說明了,假設(shè)我們 LinkedBlockingQueue 里面放的元素是這樣的:

畫圖出來就是這樣的:

現(xiàn)在我們要對(duì)這個(gè)鏈表進(jìn)行迭代,對(duì)應(yīng)到畫圖就是這樣的:
linkedBlockingQueue.iterator();

看到這個(gè)圖的時(shí)候,問題就來了:current 指針是什么時(shí)候冒出來的呢?
current,這個(gè)變量是在生成迭代器的時(shí)候就初始化好了的,指向的是 head.next:

然后 current 是通過 nextNode 這個(gè)方法進(jìn)行維護(hù)的:

正常迭代下,每調(diào)用一次都會(huì)返回 s,而 s 又是 p.next,即下一個(gè)節(jié)點(diǎn):

所以,每次調(diào)用之后 current 都會(huì)移動(dòng)一格:

這種情況,完全就沒有這個(gè)分支的事兒:

什么時(shí)候才會(huì)和它扯上關(guān)系呢?
你想象一個(gè)場(chǎng)景。
A 線程剛剛要對(duì)這個(gè)隊(duì)列進(jìn)行迭代,而 B 線程同時(shí)在對(duì)這個(gè)隊(duì)列進(jìn)行 remove。
對(duì)于 A 線程,剛剛開始迭代,畫圖是這樣的:

然后 current 還沒開始移動(dòng)呢,B 線程“咔咔”幾下,直接就把 1,2,3 全部給干出隊(duì)列了,于是站在 B 線程的視角,隊(duì)列是這樣的了:

到這里,你先思考一個(gè)問題:1,2,3 這幾個(gè)節(jié)點(diǎn),不管是自己指向自己,還是指向一個(gè) null,此時(shí)發(fā)生一個(gè) YGC 它們還在不在?
2 和 3 指定是沒了,但是 1 可不能被回收了啊。
因?yàn)殡m然元素為 1 的節(jié)點(diǎn)出隊(duì)了,但是站在 A 線程的視角,它還持有一個(gè) current 引用呢,它還是“可達(dá)”的。
所以,這個(gè)時(shí)候 A 線程開始迭代,雖然 1 被 B 出隊(duì)了,但是它一樣會(huì)被輸出。
然后,我們?cè)賮韺?duì)于下面這兩種情況,A 線程會(huì)如何進(jìn)行迭代:

當(dāng) 1 節(jié)點(diǎn)的 next 指為 null 的時(shí)候,即 p.next 為 null,那么滿足 s==null 的判斷,所以 nextNode 方法就會(huì)返回 s,也就是返回了 null:

當(dāng)你調(diào)用 hasNext 方法判斷是否還有下一節(jié)點(diǎn)的時(shí)候,就會(huì)返回 false,循環(huán)就結(jié)束了:

然后,我們站在上帝視角是知道的,后面還有 4 和 5 沒輸出呢,所以這樣就會(huì)出現(xiàn)問題。
但是,當(dāng) 1 節(jié)點(diǎn)的 next 指向自己的時(shí)候,有趣的事情就來了:

current 指針就變成了 head.next。
而你看看當(dāng)前的這個(gè)鏈表里面 head.next 是啥?

不就是 4 節(jié)點(diǎn)嗎?
這不就銜接上了嗎?
所以最終 A 線程會(huì)輸出 1,4,5。
雖然我們知道 1 元素其實(shí)已經(jīng)出隊(duì)了,但是 A 線程開始迭代的時(shí)候,它至少還在。
這玩意就體現(xiàn)了前面提到的:weakly consistent iterator,弱一致性迭代器。
這個(gè)時(shí)候,你再結(jié)合者迭代器上的注解去看,就能搞得明明白白了:

如果 hasNext 方法返回為 true,那么就必須要有下一個(gè)節(jié)點(diǎn)。即使這個(gè)節(jié)點(diǎn)被比如 take 等等的方法給移除了,也需要返回它。這就是 weakly-consistent iterator。
然后,你再看看整個(gè)類開始部分的 Java doc,其實(shí)我整篇文章就是對(duì)于這一段描述的翻譯和擴(kuò)充:

看完并理解我這篇文章之后,你再去看這部分的 Java doc,你就知道它是在說個(gè)啥事情,以及它為什么要這樣的去做這件事情了。
好了,看到這里,你現(xiàn)在應(yīng)該明白了,為什么必須要有 h.next=h,為什么不能是 h.next=null 了吧?
明白了就好。
因?yàn)楸疚木偷竭@里就要結(jié)束了。
如果你還沒明白,不要懷疑自己,大膽的說出來:什么玩意?寫的彎彎繞繞的,看求不懂。呸,垃圾作者。

最后,我還想要說的是,關(guān)于 LBQ 這個(gè)隊(duì)列,我之前也寫過這篇文章專門說它:《喜提JDK的BUG一枚!多線程的情況下請(qǐng)謹(jǐn)慎使用這個(gè)類的stream遍歷?!?/a>
文章里面也提到了 dequeue 這個(gè)方法:

但是當(dāng)時(shí)我完全沒有思考到文本提到的問題,順著代碼就捋過去了。
我覺得看到這部分代碼,然后能提出本文中這兩個(gè)問題的人,才是在帶著自己思考深度閱讀源碼的人。
解決問題不厲害,提出問題才是最屌的,因?yàn)楫?dāng)一個(gè)問題提出來的時(shí)候,它就已經(jīng)被解決了。
帶著質(zhì)疑的眼光看代碼,帶著求真的態(tài)度去探索,與君共勉之。

· · · · · · · · · · · · · · ? ? E N D ? ? · · · · · · · · · · · · · ·

推薦?? : 記錄一次非常麻煩又磨人的調(diào)試...
推 薦 ?? : 扯 下 @ E v e n t L i s t e n e r 這 個(gè) 注 解 的 神 秘 面 紗 。
推 薦 ?? : 我 試 圖 通 過 這 篇 文 章 , 教 會(huì) 你 一 種 閱 讀 源 碼 的 方 式 。
推 薦 ?? : 不 過 是 享 受 了 互 聯(lián) 網(wǎng) 的 十 年 紅 利 期 而 已 。
推 薦 ?? : ? 一 個(gè) 普 通 程 序 員 磕 磕 絆 絆 , 又 閃 閃 發(fā) 光 的 十 年 。
