你好呀,我是歪歪。
這次給你盤一個特別有意思的源碼,正如我標(biāo)題說的那樣:看懂這行源碼之后,我不禁鼓起掌來,直呼祖師爺牛逼。
這行源碼是這樣的:
java.util.concurrent.LinkedBlockingQueue#dequeue
h.next = h,不過是一個把下一個節(jié)點指向自己的動作而已。
這行代碼后面的注釋“help GC”其實在 JDK 的源碼里面也隨處可見。
不管怎么看都是一行平平無奇的代碼和隨處可見的注釋而已。
但是這行代碼背后隱藏的故事,可就太有意思了,真的牛逼,兒豁嘛。
它在干啥。
首先,我們得先知道這行代碼所在的方法是在干啥,然后再去分析這行代碼的作用。
所以老規(guī)矩,先搞個 Demo 出來跑跑:
在 LinkedBlockingQueue 的 remove 方法中就調(diào)用了 dequeue 方法,調(diào)用鏈路是這樣的:
這個方法在 remove 的過程中承擔(dān)一個什么樣的角色呢?
這個問題的答案可以在方法的注釋上找到:
這個方法就是從隊列的頭部,刪除一個節(jié)點,其他啥也不干。
就拿 Demo 來說,在執(zhí)行這個方法之前,我們先看一下當(dāng)前這個鏈表的情況是怎么樣的:
這是一個單向鏈表,然后 head 結(jié)點里面沒有元素,即 item=null,對應(yīng)做個圖出來就是這樣的:
當(dāng)執(zhí)行完這個方法之后,鏈表變成了這樣:
再對應(yīng)做個圖出來,就是這樣的:
可以發(fā)現(xiàn) 1 沒了,因為它是真正的“頭節(jié)點”,所以被 remove 掉了。
這個方法就干了這么一個事兒。
雖然它一共也只有六行代碼,但是為了讓你更好的入戲,我決定先給你逐行講解一下這個方法的代碼,講著講著,你就會發(fā)現(xiàn),誒,問題它就來了。
首先,我們回到方法入口處,也就是回到這個時候:
前兩行方法是這樣的:
對應(yīng)到圖上,也就是這樣的:
h 對應(yīng)的是 head 節(jié)點 first 對應(yīng)的是 “1” 節(jié)點
然后,來到第三行:
h 的 next 還是 h,這就是一個自己指向自己的動作,對應(yīng)到圖上是這樣的:
然后,第四行代碼:
把 first 變成 head:
最后,第五行和第六行:
拿到 first 的 item 值,作為方法的返回值。然后再把 first 的 item 值設(shè)置為 null。
對應(yīng)到圖中就是這樣,第五行的 x 就是 1,第六行執(zhí)行完成之后,圖就變成了這樣:
整個鏈表就變成了這樣:
那么現(xiàn)在問題來了:
如果我們沒有 h.next=h 這一行代碼,會出現(xiàn)什么問題呢?
我也不知道,但是我們可以推演一下:
也就是最終我們得到的是這樣的一個鏈表:
這個時候我們發(fā)現(xiàn),由于 head 指針的位置已經(jīng)發(fā)生了變化,而且這個鏈表又是一個單向鏈表,所以當(dāng)我們使用這個鏈表的時候,沒有任何問題。
而這個對象:
已經(jīng)沒有任何指針指向它了,那么它不經(jīng)過任何處理,也是可以被 GC 回收掉的。
對嗎?
你細(xì)細(xì)的品一品,是不是這個道理,從 GC 的角度來說它確實是“不可達了”,確實可以被回收掉了。
所以,當(dāng)時有人問了我這樣的一個問題:
我經(jīng)過上面的一頓分析,發(fā)現(xiàn):嗯,確實是這樣的,確實沒啥卵用啊,不寫這一行代碼,功能也是完成正常的。
但是當(dāng)時我是這樣回復(fù)的:
我沒有把話說滿,因為這一行故意寫了一行“help GC”的注釋,可能有 GC 方面的考慮。
那么到底有沒有 GC 方面的考慮,是怎么考慮的呢?
憑借著我這幾年寫文章的敏銳嗅覺,我覺得這里“大有文章”,于是我?guī)е@個問題,在網(wǎng)上溜達了一圈,還真有收獲。
help GC?
首先,一頓搜索,排除了無數(shù)個無關(guān)的線索之后,我在 openjdk 的 bug 列表里面定位到了這樣的一個鏈接:
https://bugs.openjdk.org/browse/JDK-6805775
點擊進這個鏈接的原因是標(biāo)題當(dāng)時就把吸引到了,翻譯過來就是說:LinkedBlockingQueue 的節(jié)點應(yīng)該在成為“垃圾”之前解除自己的鏈接。
先不管啥意思吧,反正 LinkedBlockingQueue、Nodes、unlink、garbage 這些關(guān)鍵詞是完全對上了。
于是我看了一下描述部分,主要關(guān)心到了這兩個部分:
看到標(biāo)號為 ① 的地方,我才發(fā)現(xiàn)在 JDK 6 里面對應(yīng)實現(xiàn)是這樣的:
而且當(dāng)時的方法還是叫 extract 而不是 dequeue。
這個方法名稱的變化,也算是一處小細(xì)節(jié)吧。
dequeue 是一個更加專業(yè)的叫法:
仔細(xì)看 JDK 6 中的 extract 方法,你會發(fā)現(xiàn),根本就沒有 help GC 這樣的注釋,也沒有相關(guān)的代碼。
它的實現(xiàn)方式就是我前面畫圖的這種:
也就是說這行代碼一定是出于某種原因,在后面的 JDK 版本中加上的。那么為什么要進行標(biāo)號為 ① 處那樣的修改呢?
標(biāo)號為 ② 的地方給到了一個鏈接,說是這個鏈接里面有關(guān)于這個問題深入的討論。
For details and in-depth discussion, see:
http://thread.gmane.org/gmane.comp.java.jsr.166-concurrency/5758
我非常確信我找對了地方,而且我要尋找的答案就在這個鏈接里面。
但是當(dāng)我點過去的時候,我發(fā)現(xiàn)不管怎么訪問,這個鏈接訪問不到了...
雖然這里的線索斷了,但是順藤摸瓜,我找到了這個 BUG 鏈接:
https://bugs.openjdk.org/browse/JDK-6806875
這兩個 BUG 鏈接說的其實是同一個事情,但是這個鏈接里面給了一個示例代碼。
這個代碼比較長,我給你截個圖,你先不用細(xì)看,只是對比我框起來的兩個部分,你會發(fā)現(xiàn)這兩部分的代碼其實是一樣的:
當(dāng) LinkedBlockingQueue 里面加入了 h.next=null 的代碼,跑上面的程序,輸出結(jié)果是這樣:
但是,當(dāng) LinkedBlockingQueue 使用 JDK 6 的源碼跑,也就是沒有 h.next=null 的代碼跑上面的程序,輸出結(jié)果是這樣:
產(chǎn)生了 47 次 FGC。
這個代碼,在我的電腦上跑,我用的是 JDK 8 的源碼,然后注釋掉 h.next = h 這行代碼,只是會觸發(fā)一次 FGC,時間差距是 2 倍:
加上 h.next = h,兩次時間就相對穩(wěn)定:
好,到這里,不管原理是什么,我們至少驗證了,在這個地方必須要 help GC 一下,不然確實會有性能影響。
但是,到底是為什么呢?
在反復(fù)仔細(xì)的閱讀了這個 BUG 的描述部分之后,我大概懂了。
最關(guān)鍵的一個點其實是藏在了前面示例代碼中我標(biāo)注了五角星的那一行注釋:
SAME test, but create the queue before GC, head node will be in old gen(頭節(jié)點會進入老年代)
我大概知道問題的原因是因為“head node will be in old gen”,但是具體讓我描述出來我也有點說不出來。
說人話就是:我懂一點,但是不多。
于是又經(jīng)過一番查找,我找到了這個鏈接,在這里面徹底搞明白是怎么一回事了:
http://concurrencyfreaks.blogspot.com/2016/10/self-linking-and-latency-life-of.html
在這個鏈接里面提到了一個視頻,它讓我從第 23 分鐘開始看:
我看了一下這個視頻,應(yīng)該是 2015 年發(fā)布的。因為整個會議的主題是:20 years of Java, just the beginning:
https://www.infoq.com/presentations/twitter-services/
這個視頻的主題是叫做“Life if a twitter JVM engineer”,是一個 twitter 的 JVM 工程師在大會分享的在工作遇到的一些關(guān)于 JVM 的問題。
雖然是全程英文,但是你知道的,我的 English level 還是比較 high 的。
日常聽說,問題不大。所以大概也就聽了個幾十遍吧,結(jié)合著他的 PPT 也就知道關(guān)于這個部分他到底在分享啥了。
我要尋找的答案,也藏在這個視頻里面。
我挑關(guān)鍵的給你說。
首先他展示了這樣的這個圖片:
老年代的 x 對象指向了年輕代的 y 對象。一個非常簡單的示意圖,他主要是想要表達“跨代引用”這個問題。
然后,出現(xiàn)了這個圖片:
這里的 Queue 就是本文中討論的 LinkedBlockingQueue。
首先可以看到整個 Queue 在老年代,作為一個隊列對象,極有可能生命周期比較長,所以隊列在老年代是一個正常的現(xiàn)象。
然后我們往這個隊列里面插入了 A,B 兩個元素,由于這兩個元素是我們剛剛插入的,所以它們在年輕代,也沒有任何毛病。
此時就出現(xiàn)了老年代的 Queue 對象,指向了位于年輕代的 A,B 節(jié)點,這樣的跨代引用。
接著,A 節(jié)點被干掉了,出隊:
A 出隊的時候,由于它是在年輕代的,且沒有任何老年代的對象指向它,所以它是可以被 GC 回收掉的。
同理,我們插入 D,E 節(jié)點,并讓 B 節(jié)點出隊:
假設(shè)此時發(fā)生一次 YGC, A,B 節(jié)點由于“不可達”被干掉了,C 節(jié)點在經(jīng)歷幾次 YGC 之后,由于不是“垃圾”,所以晉升到了老年代:
這個時候假設(shè) C 出隊,你說會出現(xiàn)什么情況?
首先,我問你:這個時候 C 出隊之后,它是否是垃圾?
肯定是的,因為它不可達了嘛。從圖片上也可以看到,C 雖然在老年代,但是沒有任何對象指向它了,它確實完犢子了:
好,接下來,請坐好,認(rèn)真聽了。
此時,我們加入一個 F 節(jié)點,沒有任何毛?。?/p>
接著 D 元素被出隊了:
就像下面這個動圖一樣:
我把這一幀拿出來,針對這個 D 節(jié)點,單獨的說:
假設(shè)在這個時候,再次發(fā)生 YGC,D 節(jié)點雖然出隊了,它也位于年輕代。但是位于老年代的 C 節(jié)點還指向它,所以在 YGC 的時候,垃圾回收線程不敢動它。
因此,在幾輪 YGC 之后,本來是“垃圾”的 D,搖身一變,進入老年代了:
雖然它依然是“垃圾”,但是它進入了老年代,YGC 對它束手無策,得 FGC 才能干掉它了。
然后越來越多的出隊節(jié)點,變成了這樣:
然后,他們都進入了老年代:
我們站在上帝視角,我們知道,這一串節(jié)點,應(yīng)該在 YGC 的時候就被回收掉。
但是這種情況,你讓 GC 怎么處理?
它根本就處理不了。
GC 線程沒有上帝視角,站在它的視角,它做的每一步動作都是正確的、符合規(guī)定的。最終呈現(xiàn)的效果就是必須要經(jīng)歷 FGC 才能把這些本來早就應(yīng)該回收的節(jié)點,進行回收。而我們知道,F(xiàn)GC 是應(yīng)該盡量避免的,所以這個處置方案,還是“差點意思”的。
所以,我們應(yīng)該怎么辦?
你回想一下,萬惡之源,是不是這個時候:
C 雖然被移出隊列了,但是它還持有一個下一個節(jié)點的引用,讓這個引用變成跨代引用的時候,就出毛病了。
所以,help GC,這不就來了嗎?
不管你是位于年輕代還是老年代,只要是出隊,就把你的 next 引用干掉,杜絕出現(xiàn)前面我們分析的這種情況。
這個時候,你再回過頭去看前面提到的這句話:
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 都能達到 help GC 的目的,那么為什么最終的寫法是 h.hext=h 呢?
或者換句話說:為什么是 h.next=h,而不是 h.next=null 呢?
針對這個問題,我也盯著源碼,仔細(xì)思考了很久,最終得出了一個“非常大膽”的結(jié)論是:這兩個寫法是一樣的,不過是編碼習(xí)慣不一樣而已。
但是,注意,我要說但是了。
再次經(jīng)過一番查詢、分析和論證,這個地方它還必須得是 h.next=h。
因為在這個 bug 下面有這樣的一句討論:
關(guān)鍵詞是:weakly consistent iterator,弱一致性迭代器。也就是說這個問題的答案是藏在 iterator 迭代器里面的。
在 iterator 對應(yīng)的源碼中,有這樣的一個方法:
java.util.concurrent.LinkedBlockingQueue.Itr#nextNode
針對 if 判斷中的 s==p,我們把 s 替換一下,就變成了 p.next=p:
那么什么時候會出現(xiàn) p.next=p 這樣的代碼呢?
答案就藏在這個方法的注釋部分:dequeued nodes (p.next == p)
dequeue 這不是巧了嗎,這不是和前面給呼應(yīng)起來了嗎?
好,到這里,我要開始給你畫圖說明了,假設(shè)我們 LinkedBlockingQueue 里面放的元素是這樣的:
畫圖出來就是這樣的:
現(xiàn)在我們要對這個鏈表進行迭代,對應(yīng)到畫圖就是這樣的:
linkedBlockingQueue.iterator();
看到這個圖的時候,問題就來了:current 指針是什么時候冒出來的呢?
current,這個變量是在生成迭代器的時候就初始化好了的,指向的是 head.next:
然后 current 是通過 nextNode 這個方法進行維護的:
正常迭代下,每調(diào)用一次都會返回 s,而 s 又是 p.next,即下一個節(jié)點:
所以,每次調(diào)用之后 current 都會移動一格:
這種情況,完全就沒有這個分支的事兒:
什么時候才會和它扯上關(guān)系呢?
你想象一個場景。
A 線程剛剛要對這個隊列進行迭代,而 B 線程同時在對這個隊列進行 remove。
對于 A 線程,剛剛開始迭代,畫圖是這樣的:
然后 current 還沒開始移動呢,B 線程“咔咔”幾下,直接就把 1,2,3 全部給干出隊列了,于是站在 B 線程的視角,隊列是這樣的了:
到這里,你先思考一個問題:1,2,3 這幾個節(jié)點,不管是自己指向自己,還是指向一個 null,此時發(fā)生一個 YGC 它們還在不在?
2 和 3 指定是沒了,但是 1 可不能被回收了啊。
因為雖然元素為 1 的節(jié)點出隊了,但是站在 A 線程的視角,它還持有一個 current 引用呢,它還是“可達”的。
所以,這個時候 A 線程開始迭代,雖然 1 被 B 出隊了,但是它一樣會被輸出。
然后,我們再來對于下面這兩種情況,A 線程會如何進行迭代:
當(dāng) 1 節(jié)點的 next 指為 null 的時候,即 p.next 為 null,那么滿足 s==null 的判斷,所以 nextNode 方法就會返回 s,也就是返回了 null:
當(dāng)你調(diào)用 hasNext 方法判斷是否還有下一節(jié)點的時候,就會返回 false,循環(huán)就結(jié)束了:
然后,我們站在上帝視角是知道的,后面還有 4 和 5 沒輸出呢,所以這樣就會出現(xiàn)問題。
但是,當(dāng) 1 節(jié)點的 next 指向自己的時候,有趣的事情就來了:
current 指針就變成了 head.next。
而你看看當(dāng)前的這個鏈表里面 head.next 是啥?
不就是 4 節(jié)點嗎?
這不就銜接上了嗎?
所以最終 A 線程會輸出 1,4,5。
雖然我們知道 1 元素其實已經(jīng)出隊了,但是 A 線程開始迭代的時候,它至少還在。
這玩意就體現(xiàn)了前面提到的: weakly consistent iterator,弱一致性迭代器。
這個時候,你再結(jié)合者迭代器上的注解去看,就能搞得明明白白了:
如果 hasNext 方法返回為 true,那么就必須要有下一個節(jié)點。即使這個節(jié)點被比如 take 等等的方法給移除了,也需要返回它。這就是 weakly-consistent iterator。
然后,你再看看整個類開始部分的 Java doc,其實我整篇文章就是對于這一段描述的翻譯和擴充:
看完并理解我這篇文章之后,你再去看這部分的 Java doc,你就知道它是在說個啥事情,以及它為什么要這樣的去做這件事情了。
好了,看到這里,你現(xiàn)在應(yīng)該明白了,為什么必須要有 h.next=h,為什么不能是 h.next=null 了吧?
明白了就好。
因為本文就到這里就要結(jié)束了。
如果你還沒明白,不要懷疑自己,大膽的說出來:什么玩意?寫的彎彎繞繞的,看求不懂。呸,垃圾作者。
最后,我還想要說的是,關(guān)于 LBQ 這個隊列,我之前也寫過這篇文章專門說它:《喜提JDK的BUG一枚!多線程的情況下請謹(jǐn)慎使用這個類的stream遍歷?!?/p>
文章里面也提到了 dequeue 這個方法:
但是當(dāng)時我完全沒有思考到文本提到的問題,順著代碼就捋過去了。
我覺得看到這部分代碼,然后能提出本文中這兩個問題的人,才是在帶著自己思考深度閱讀源碼的人。
解決問題不厲害,提出問題才是最屌的,因為當(dāng)一個問題提出來的時候,它就已經(jīng)被解決了。文章來源:http://www.zghlxwxcb.cn/news/detail-445230.html
帶著質(zhì)疑的眼光看代碼,帶著求真的態(tài)度去探索,與君共勉之。文章來源地址http://www.zghlxwxcb.cn/news/detail-445230.html
到了這里,關(guān)于我試圖通過這篇文章告訴你,這行源碼有多牛逼。的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!