震驚!ConcurrentHashMap里面也有死循環(huán),作者留下的“彩蛋”了解一下?
掃描二維碼
隨時(shí)隨地手機(jī)看文章
荒腔走板
大家好,我是why。
時(shí)間過的真是快,一周又要結(jié)束了。那么,你比上周更博學(xué)了嗎?先來(lái)一個(gè)簡(jiǎn)短的荒腔走板,給冰冷的技術(shù)文注入一絲色彩。
上面這張圖是我之前在南五環(huán),路過南苑機(jī)場(chǎng)的時(shí)候拍的。
這是一架飛機(jī)在降落,拍的時(shí)候我一下就想起了李志的《天空之城》
飛機(jī)飛過天空,天空之城
落雨下的黃昏的我們
此刻我在異鄉(xiāng)的夜里
感覺著你忽明忽暗
我想回到過去,沉默著歡喜
天空之城在哭泣越來(lái)越明亮的你
這位南京李先生算不上一個(gè)名人,只是在一個(gè)小圈子里面比較出名。但是呢,總有一些綜藝會(huì)在節(jié)目里面未經(jīng)許可直接使用他的歌曲。
簡(jiǎn)單來(lái)說(shuō)就是被侵權(quán)了。他每次都會(huì)去維權(quán)。
有的時(shí)候會(huì)得到道歉,有的時(shí)候胳膊擰不過大腿。
李志維權(quán)的時(shí)候說(shuō)過:我能做的其實(shí)挺少,但是每一次我試圖去做一些事情時(shí),都會(huì)有朋友跟我說(shuō),這樣沒用的,你看還是這個(gè)死樣,所有人都很絕望,我當(dāng)然也絕望,但是我始終想,能拯救一個(gè)是一個(gè),能教育一個(gè)是一個(gè),哪有什么事情一夜之間全改了,都要慢慢來(lái)。
現(xiàn)在他是一位 404 歌手了。
正如他之前說(shuō)過的這樣:我就怕我哪一天不愛這個(gè)世界了,只愛我自己,過自己小日子,跟他們一樣,賺點(diǎn)錢,戴個(gè)面具。但是那一天真來(lái)的話我也沒辦法。
我記得網(wǎng)易云音樂里面他的歌下面有一條評(píng)論是這樣的:關(guān)于李志我不想說(shuō)太多,反正我知道,你聽李志的歌的時(shí)候你想的那個(gè)人是不會(huì)和你在一起的。
我覺得不是這樣的,我聽李志的時(shí)候我沒有想起誰(shuí)。我只是想到了過往的真實(shí)而用力的生活。
所以,這句話應(yīng)該是:關(guān)于李志我不想說(shuō)太多,反正我知道,你聽李志的歌的時(shí)候你想到的都是你正在經(jīng)歷的,或者懷念的而又回不去的生活。
總之逼哥牛逼。
好了,說(shuō)回文章。
JDK BUG
這篇文章,聊一下我最近才知道的一個(gè)關(guān)于 JDK 8 的 BUG 吧。
首先說(shuō)一下我是怎么發(fā)現(xiàn)這個(gè) BUG 的呢?
大家都知道我對(duì) Dubbo 有一定的關(guān)注,前段時(shí)間 Dubbo 2.7.7 發(fā)布后我看了它的更新點(diǎn),就是下面這個(gè)網(wǎng)址:
https://github.com/apache/dubbo/releases/tag/dubbo-2.7.7
其中有 Bugfixex 這一部分:
每一個(gè)我都去簡(jiǎn)單的看了一下,其他的 Bugfixes 或多或少都和 Dubbo 框架有一定的關(guān)聯(lián)性。但是上面紅框框起來(lái)的部分完全就是 JDK 的 Bug 了。
所以可以單獨(dú)拎出來(lái)說(shuō)。
這個(gè) Bug 我也是看到了這個(gè)地方才知道的,但是研究的過程中我發(fā)現(xiàn),這個(gè)怎么說(shuō)呢:我懷疑這根本就不是 Bug ,這就是 Doug Lea 老爺子在釣魚執(zhí)法。
為什么這樣的說(shuō)呢,大家看完本文就知道了。
Bug 穩(wěn)定復(fù)現(xiàn)
點(diǎn)擊 Dubbo 里面的鏈接,我們可以看到具體的描述就是一個(gè)鏈接:
打開這個(gè)鏈接:
https://bugs.openjdk.java.net/browse/JDK-8062841
我們可以看到:這個(gè) Bug 是位于大名鼎鼎的 concurrent 包里面的 computeIfAbsent 方法。
這個(gè) Bug 在 JDK 9 里面被修復(fù)了,修復(fù)人是 Doug Lea。
而我們知道 ConcurrentHashMap 就是 Doug Lea 的大作,可以說(shuō)是“誰(shuí)污染誰(shuí)治理”。
要了解這個(gè) Bug 是怎么回事,就必須先了解下面這個(gè)方法是干啥的:
java.util.concurrent.ConcurrentHashMap#computeIfAbsent
從這個(gè)方法的第二個(gè)入?yún)?mappingFunction 我們可以知道這是 JDK 8 之后提供的方法了。
該方法的含義是:當(dāng)前 Map 中 key 對(duì)應(yīng)的值不存在時(shí),會(huì)調(diào)用 mappingFunction 函數(shù),并且將該函數(shù)的執(zhí)行結(jié)果(不為 null)作為該 key 的 value 返回。
比如下面這樣的:
初始化一個(gè) ConcurrentHashMap ,然后第一次去獲取 key 為 why 的 value,沒有獲取到,直接返回 null。
接著調(diào)用 computeIfAbsent 方法,獲取到 null 后調(diào)用 getValue 方法,將該方法的返回值和當(dāng)前的 key 關(guān)聯(lián)起來(lái)。
所以,第二次獲取的時(shí)候拿到了 “why技術(shù)”。
其實(shí)上面的代碼的 17 行的返回值就是 “why技術(shù)”,只是我為了代碼演示,再去調(diào)用了一次 map.get() 方法。
知道這個(gè)方法干什么的,接下來(lái)就帶大家看看 Bug 是什么。
我們直接用這個(gè)問題里面給的測(cè)試用例,地址:
https://bugs.openjdk.java.net/secure/attachment/23985/Main.java
我只是在第 11 行和第 21 行加入了輸出語(yǔ)句:
正常的情況下,我們希望方法正常結(jié)束,然后 map 里面是這樣的:{AaAa=42,BBBB=42}
但是你把這個(gè)代碼拿到本地去跑(需要 JDK 8 環(huán)境),你會(huì)發(fā)現(xiàn),這個(gè)方法永遠(yuǎn)不會(huì)結(jié)束。因?yàn)樗谶M(jìn)行死循環(huán)。
這就是 Bug。
提問的藝術(shù)
知道 Bug 了,按理來(lái)說(shuō)就應(yīng)該開始分析源碼,了解為啥出現(xiàn)了會(huì)出現(xiàn)這個(gè) Bug。
但是我想先插播一小節(jié)提問的藝術(shù)。因?yàn)檫@個(gè) Bug 就是一個(gè)活生生的示例呀。
這個(gè)鏈接,我建議你打開看看,這里面還有 Doug Lea 老爺子的親自解答:
https://bugs.openjdk.java.net/browse/JDK-8062841
首先我們看提出問題的這個(gè)人對(duì)于問題的描述(可以先不用細(xì)看,反正看著也是懵逼的):
通常情況下,被提問的人分為兩類人:
遇到過并知道這個(gè)問題的人,可以看的明白你在說(shuō)什么。
雖然沒有碰見過這個(gè)問題,但感覺是自己熟悉的領(lǐng)域,可能知道答案,但是看了你的問題描述,也不知道你在說(shuō)什么。
這個(gè)描述很長(zhǎng),我第一次看的時(shí)候很懵逼,很難理解他在說(shuō)什么。我就是屬于第二類人。
而且在大多數(shù)的問題中,第二類人比第一類人多很多。
但是當(dāng)我了解到這個(gè) Bug 的來(lái)龍去脈的時(shí)候,再看這個(gè)描述,其實(shí)寫的很清楚了,也很好理解。我就變成第一類人了。
但是變成第一類人是有前提的,前提就是我已經(jīng)了解到了這個(gè)地方 Bug 了??上?,現(xiàn)在是提問,而被提問的人,還對(duì)這個(gè) Bug 不是特別了解。
即使,這個(gè)被提問的人是 Doug Lea。
可以看到,2014 年 11 月 04 日 Martin 提出這個(gè)問題后, Doug Lea 在不到一個(gè)小時(shí)內(nèi)就進(jìn)行了回復(fù),我給大家翻譯一下,老爺子回復(fù)的啥:
首先,你說(shuō)你發(fā)現(xiàn)了 ConcurrentHashMap 的問題,但是我沒有看到的測(cè)試用例。那么我就猜測(cè)一下是不是有其他線程在計(jì)算值的時(shí)候被卡住了,但是從你的描述中我也看不到相應(yīng)的點(diǎn)。
簡(jiǎn)單來(lái)說(shuō)就是:Talk is cheap. Show me the code.(屁話少說(shuō),放碼過來(lái)。)
于是另一個(gè)哥們 Pardeep 在一個(gè)月后提交了一個(gè)測(cè)試案例,就是我們前面看到的測(cè)試案例:
Pardeep 給 Martin 回復(fù)到下面這段話:
他開門見山的說(shuō):我注意這個(gè) bug 很長(zhǎng)時(shí)間了,然后我還有一個(gè)測(cè)試用例。
可以說(shuō)這個(gè)測(cè)試案例的出現(xiàn),才是真正的轉(zhuǎn)折點(diǎn)。
然后他提出了自己的看法,這段描述簡(jiǎn)短有力的說(shuō)出了問題的所在(后面我們會(huì)講到),然后他還提出了自己的意見。
不到一個(gè)小時(shí),這個(gè)回到得到了 Doug Lea 的回復(fù):
他說(shuō):小伙子的建議還是不錯(cuò)的,但是現(xiàn)在還不是我們解決這個(gè)問題的時(shí)候。我們也許會(huì)通過代碼改進(jìn)死鎖檢查機(jī)制,以幫助用戶 debug 他們的程序。但是目前而言,這種機(jī)制就算做出來(lái),工作效率也是非常低下的,比如在當(dāng)前的這個(gè)案例下。但是現(xiàn)在我們至少清楚的知道,是否要實(shí)現(xiàn)這種機(jī)制是不能確定的。
總之一句話:?jiǎn)栴}我知道了,但是目前我還沒想到好的解決方法。
但是,在 19 天以后,老爺子又回來(lái)處理這個(gè)問題了:
這次的回答可謂是峰回路轉(zhuǎn),他說(shuō):請(qǐng)忽略我之前的話。我們發(fā)現(xiàn)了一些可行的改進(jìn)方法,這些改進(jìn)可以處理更多的用戶錯(cuò)誤,包括本報(bào)告中所提供的測(cè)試用例,即解決在 computeIfAbsent 中提供的函數(shù)中進(jìn)行遞歸映射更新導(dǎo)致死鎖這樣的問題。我們會(huì)在 JDK 9 里面解決這個(gè)問題。
所以,回顧這個(gè) Bug 被提出的過程。
首先是 Martin 提出了這個(gè)問題,并進(jìn)行了詳細(xì)的描述??上У氖撬拿枋龊軐I(yè),是站在你已經(jīng)了解了這個(gè) Bug 的立場(chǎng)上去描述的,讓人看的很懵逼。
所以 Doug Lea 看到后也表示這啥呀,沒搞懂。
然后是 Pardeep 跟進(jìn)這個(gè)問題,轉(zhuǎn)折點(diǎn)在于他拋出的這個(gè)測(cè)試案例。而我相信,既然 Martin 能把這個(gè)問題描述的很清楚,他一定是有一個(gè)自己的測(cè)試案例的,但是他沒有展現(xiàn)出來(lái)。
所以,朋友們,測(cè)試案例的重要性不言而喻了。問問題的時(shí)候不要只是拋出異常,你至少給段對(duì)應(yīng)的代碼,或者日志,或者一次性描述清楚,寫在文檔里面發(fā)出來(lái)也行呀。
Bug 的原因
導(dǎo)致這個(gè) Bug 的原因也是一句話就能說(shuō)清楚,前面的 Pardeep 老哥也說(shuō)了:
問題在于我們?cè)谶M(jìn)行 computeIfAbsent 的時(shí)候,里面還有一個(gè) computeIfAbsent。而這兩個(gè) computeIfAbsent 它們的 key 對(duì)應(yīng)的 hashCode 是一樣的。
你說(shuō)巧不巧。
當(dāng)它們的 hashCode 是一樣的時(shí)候,說(shuō)明它們要往同一個(gè)槽放東西。
而當(dāng)?shù)诙€(gè)元素進(jìn)來(lái)的時(shí)候,發(fā)現(xiàn)坑位已經(jīng)被前一個(gè)元素占領(lǐng)了,可能就是這樣的畫風(fēng):
接下來(lái)我們就解析一下 computeIfAbsent 方法的工作流程:
第一步是計(jì)算 key 對(duì)應(yīng)的 hashCode 應(yīng)該放到哪個(gè)槽里面。
然后是進(jìn)入1649 行的這個(gè) for 循環(huán),而這個(gè) for 循環(huán)是一個(gè)死循環(huán),它在循環(huán)體內(nèi)部判斷各種情況,如果滿足條件則 break 循環(huán)。
首先,我們看一下 “AaAa” 和 “BBBB” 經(jīng)過 spread 計(jì)算(右移 16 位高效計(jì)算)后的 h 值是什么:
哇塞,好巧啊,從框起來(lái)的這兩部分可以看到,都是 2031775 呢。
說(shuō)明他們要在同一個(gè)槽里面搞事情。
先是 “AaAa” 進(jìn)入 computeIfAbsent 方法:
在第一次循環(huán)的時(shí)候 initTable,沒啥說(shuō)的。
第二次循環(huán)先是在 1653 行計(jì)算出數(shù)組的下標(biāo),并取出該下標(biāo)的 node。發(fā)現(xiàn)這個(gè) node 是空的。于是進(jìn)入分支判斷:
在標(biāo)號(hào)為 ① 的地方進(jìn)行 cas 操作,先用 r(即 ReservationNode)進(jìn)行一個(gè)占位的操作。
在標(biāo)號(hào)為 ② 的地方進(jìn)行 mappingFunction.apply 的操作,計(jì)算 value 值。如果計(jì)算出來(lái)不為 null,則把 value 組裝成最終的 node。
在標(biāo)號(hào)為 ③ 的東西把之前占位的 ReservationNode 替換成標(biāo)號(hào)為 ② 的地方組裝成的node 。
問題就出現(xiàn)標(biāo)號(hào)為 ② 的地方??梢钥吹竭@里去進(jìn)行了 mappingFunction.apply 的操作,而這個(gè)操作在我們的案例下,會(huì)觸發(fā)另一次 computeIfAbsent 操作。
現(xiàn)在 “AaAa” 就等著這個(gè) computeIfAbsent 操作的返回值,然后進(jìn)行下一步操作,也就是進(jìn)行標(biāo)號(hào)為 ③ 的操作了。
接著 “BBBB” 就來(lái)了。
通過前面我們知道了 “BBBB” 的 hashCode 經(jīng)過計(jì)算后也是和 “AaAa” 一樣。所以它也要想要去那個(gè)槽里面搞事情。
可惜它來(lái)晚了一步。
帶大家看一下對(duì)應(yīng)的代碼:
當(dāng) key 為 “BBBB” 的時(shí)候,算出來(lái)的 h 值也是 2031775。
它也會(huì)進(jìn)入 1649 行的這個(gè)死循環(huán)。然后進(jìn)行各種判斷。
接下來(lái)我要論證的是:
在本文的示例代碼中,當(dāng)運(yùn)行到 key 為 “BBBB” 的時(shí)候,進(jìn)入 1649 行這個(gè)死循環(huán)后,就退不出來(lái)了。程序一直在里面循環(huán)運(yùn)行。
在標(biāo)號(hào)為 ① 的地方,由于這個(gè)時(shí)候 tab 已經(jīng)不為 null 了,所以不會(huì)進(jìn)入這個(gè)分支。
在標(biāo)號(hào)為 ② 的地方,由于之前 “AaAa” 已經(jīng)扔了一個(gè) ReservationNode 進(jìn)去占位置了,所以不等于 null。所以,也就不會(huì)進(jìn)入這個(gè)分支。
怕你懵逼,給你配個(gè)圖,真是暖男作者石錘了:
接下來(lái)到標(biāo)號(hào)為 ③ 的地方,里面有一個(gè) MOVED,這個(gè) MOVED 是干啥的呢?
表示當(dāng)前的 ConcurrentHashMap 是否是在進(jìn)行擴(kuò)容。
很明顯,現(xiàn)在還沒有到該擴(kuò)容的時(shí)候:
第 1678 行的 f 就是之前 “AaAa” 扔進(jìn)去的 ReservationNode ,這個(gè) Node 的 hash 是 -3,不等于MOVED(-1)。
所以,不會(huì)進(jìn)入這個(gè)分支判斷。
接下來(lái),能進(jìn)的只有標(biāo)號(hào)為 ④ 的地方了,所以我們只需要把這個(gè)地方攻破,就徹底了解這個(gè) Bug 了。
走起:
通過前面的分析我們知道了,當(dāng)前案例情況下,只會(huì)進(jìn)入 1672 行這個(gè)分支。
而這個(gè)分支里面,還有四個(gè)判斷。我們一個(gè)個(gè)的攻破:
標(biāo)號(hào)為 ⑤ 的地方,tabAt 方法取出來(lái)的對(duì)象,就是之前 “AaAa” 放進(jìn)去的占位的 ReservationNode ,也就是這個(gè) f 。所以可以進(jìn)入這個(gè)分支判斷。
標(biāo)號(hào)為 ⑥ 的地方,fh >=0 。而 fh 是當(dāng)前 node 的 hash 值,大于 0 說(shuō)明當(dāng)前是按照鏈表存儲(chǔ)的數(shù)據(jù)。之前我們分析過了,當(dāng)前的 hash 值是 -3。所以,不會(huì)進(jìn)入這個(gè)分支。
標(biāo)號(hào)為 ⑦ 的地方,判斷 f 節(jié)點(diǎn)是否是紅黑樹存儲(chǔ)。當(dāng)然不是的。所以,不會(huì)進(jìn)入這個(gè)分支。
標(biāo)號(hào)為 ⑧ 的地方,binCount 代表的是該下標(biāo)里面,有幾個(gè) node 節(jié)點(diǎn)。很明顯,現(xiàn)在一個(gè)都沒有。所以當(dāng)前的 binCount 還是 0 。所以,不會(huì)進(jìn)入這個(gè)分支。
完了。分析完了。
Bug 也就出來(lái)了,一次 for 循環(huán)結(jié)束后,沒有 break。苦就苦在這個(gè) for 循環(huán)還是個(gè)死循環(huán)。
再來(lái)一個(gè)上帝視角,看看當(dāng) key 為 “BBBB” 的時(shí)候發(fā)生了什么事情:
進(jìn)入無(wú)限循環(huán)內(nèi):
①.經(jīng)過 “AaAa” 之后,tab 就不為 null 了。
②.當(dāng)前的槽中已經(jīng)被 “AaAa” 先放了一個(gè) ReservationNode 進(jìn)行占位了,所以不為 null。
③.當(dāng)前的 map 并沒有進(jìn)行擴(kuò)容操作。
④.包含⑤、⑥、⑦、⑧。
⑤.tabAt 方法取出來(lái)的對(duì)象,就是之前 “AaAa” 放進(jìn)去的占位的 ReservationNode,所以滿足條件進(jìn)入分支。
⑥.判斷當(dāng)前是否是鏈表存儲(chǔ),不滿足條件,跳過。
⑦.判斷當(dāng)前是否是紅黑樹存儲(chǔ),不滿足條件,跳過。
⑧.判斷當(dāng)前下標(biāo)里面是否放了 node,不滿足條件(“AaAa” 只有個(gè)占位的Node ,并沒有初始完成,所以還沒有放到該下標(biāo)里面),進(jìn)入下一次循環(huán)。
然后它就在死循環(huán)里面出不來(lái)了!
我相信現(xiàn)在大家對(duì)于這個(gè) Bug 的來(lái)路了解清楚了。
如果你是在 idea 里面跑這個(gè)測(cè)試用例,也可以這樣直觀的看一眼:
點(diǎn)擊這個(gè)照相機(jī)圖標(biāo):
從線程快照里面其實(shí)也是可以看到端倪的,大家可以去分析分析。
有的觀點(diǎn)說(shuō)的是由于線程安全的導(dǎo)致的死循環(huán),經(jīng)過分析我覺得這個(gè)觀點(diǎn)是不對(duì)的。
它存在死循環(huán),不是由于線程安全導(dǎo)致的,純粹是自己進(jìn)入了死循環(huán)。
或者說(shuō),這是一個(gè)“彩蛋”?
或者......自信點(diǎn),就說(shuō)這事 Bug ,能穩(wěn)定復(fù)現(xiàn)的那種。
那么我們?nèi)绻鞘褂?JDK 8 怎么避免踩到這個(gè)“彩蛋”呢?
看看 Dubbo 里面是怎么解決的:
先調(diào)用了 get 方法,如果返回為 null,則調(diào)用 putIfAbsent 方法,這樣就能實(shí)現(xiàn)和之前一樣的效果了。
如果你在項(xiàng)目中也有使用 computeIfAbsent 的地方,建議也這樣去修改。
說(shuō)到 ConcurrentHashMap get 方法返回 null,我就想起了之前討論的一個(gè)面試題了:
答案都寫在這個(gè)文章里面了,有興趣的可以了解一下《這道面試題我真不知道面試官想要的回答是什么》
Bug 的解決
其實(shí)徹底理解了這個(gè) Bug 之后,我們?cè)賮?lái)看一下 JDK 9 里面的解決方案,看一下官方源碼對(duì)比:
http://gee.cs.oswego.edu/cgi-bin/viewcvs.cgi/jsr166/src/main/java/util/concurrent/ConcurrentHashMap.java?r1=1.258&r2=1.259&sortby=date&diff_format=f
就加了兩行代碼,判斷完是否是紅黑樹節(jié)點(diǎn)后,再判斷一下是否是 ReservationNode 節(jié)點(diǎn),因?yàn)檫@個(gè)節(jié)點(diǎn)就是個(gè)占位節(jié)點(diǎn)。如果是,則拋出異常。
就這么簡(jiǎn)單。沒有什么神秘的。
所以,如果你在 JDK 9 里面執(zhí)行文本的測(cè)試用例,就會(huì)拋出 IllegalStateException。
這就是 Doug Lea 之前提到的解決方案:
了解了這個(gè) Bug 的來(lái)龍去脈后,特別是看到解決方案后,我們就能輕描淡寫的說(shuō)一句:
害,就這?沒聽說(shuō)過!
另外,我看 JDK 9 修復(fù)的時(shí)候還不止修復(fù)了一個(gè)問題:
http://hg.openjdk.java.net/jdk9/jdk9/jdk/file/6dd59c01f011/src/java.base/share/classes/java/util/concurrent/ConcurrentHashMap.java
你去翻一翻。發(fā)現(xiàn),啊,全是知識(shí)點(diǎn)啊,學(xué)不動(dòng)了。
釣魚執(zhí)法
為什么我在文章的一開始就說(shuō)了這是 Doug Lea 在釣魚執(zhí)法呢?
因?yàn)樵谧铋_始提問的藝術(shù)那一部分,我相信,Doug Lea 跑完那個(gè)測(cè)試案例之后,心里也有點(diǎn)數(shù)了。
大概知道問題在哪了,而且從他的回答和他寫的文檔中我也有理由相信,他寫的這個(gè)方法的時(shí)候就知道可能會(huì)出問題。
而且,Pardeep 的回復(fù)中提到了文檔,那我們就去看看官方文檔對(duì)于該方法的描述是怎樣的:
https://docs.oracle.com/javase/8/docs/api/
文檔中說(shuō)函數(shù)方法應(yīng)該簡(jiǎn)短,簡(jiǎn)單。而且不能在更新的映射的時(shí)候更新映射。就是說(shuō)不能套娃。
套娃,用程序說(shuō)就是recursive(遞歸),按照文檔說(shuō)如果存在遞歸,則會(huì)拋出 IllegalStateException 。
而提到遞歸,你想到了什么?
我首先就想到了斐波拉契函數(shù)。我們用 computeIfAbsent 實(shí)現(xiàn)一個(gè)斐波拉契函數(shù)如下:
public class Test {
static Map<Integer, Integer> cache = new ConcurrentHashMap<>();
public static void main(String[] args) {
System.out.println("f(" + 14 + ") =" + fibonacci(14));
}
static int fibonacci(int i) {
if (i == 0)
return i;
if (i == 1)
return 1;
return cache.computeIfAbsent(i, (key) -> {
System.out.println("Slow calculation of " + key);
return fibonacci(i - 2) + fibonacci(i - 1);
});
}
}
這就是遞歸調(diào)用,我用 JDK 1.8 跑的時(shí)候并沒有拋出 IllegalStateException,只是程序假死了,原因和我們前面分析的是一樣一樣的。我理解這個(gè)地方是和文檔不符的。
所以,我懷疑是 Doug Lea 在這個(gè)地方釣魚執(zhí)法。
CHM一定線程安全嗎?
既然都說(shuō)到 currentHashMap(CHM)了,那我說(shuō)一個(gè)相關(guān)的注意點(diǎn)吧。
首先 CHM 一定能保證線程安全嗎?
是的,CHM 本身一定是線程安全的。但是,如果你使用不當(dāng)還是有可能會(huì)出現(xiàn)線程不安全的情況。
給大家看一點(diǎn) Spring 中的源碼吧:
org.springframework.core.SimpleAliasRegistry
在這個(gè)類中,aliasMap 是 ConcurrentHashMap 類型的:
在 registerAlias 和 getAliases 方法中,都有對(duì) aliasMap 進(jìn)行操作的代碼,但是在操作之前都是用 synchronized 把 aliasMap 鎖住了。
為什么?為什么我們操作 ConcurrentHashMap 的時(shí)候還要加鎖呢?
這個(gè)是根據(jù)場(chǎng)景而定的,這個(gè)別名管理器,在這里加鎖應(yīng)該是為了避免多個(gè)線程操作 ConcurrentHashMap 。
雖然 ConcurrentHashMap 是線程安全的,但是假設(shè)如果一個(gè)線程 put,一個(gè)線程 get,在這個(gè)代碼的場(chǎng)景里面是不允許的。
如果覺得不太好理解的話我舉一個(gè) redis 的例子。
redis 的 get、set 方法都是線程安全的吧。但是你如果先 get 再 set,那么在多線程的情況下還是會(huì)有問題的。
因?yàn)檫@兩個(gè)操作不是原子性的。所以 incr 就應(yīng)運(yùn)而生了。
我舉這個(gè)例子的是想說(shuō)線程安全與否不是絕對(duì)的,要看場(chǎng)景。給你一個(gè)線程安全的容器,你使用不當(dāng)還是會(huì)有線程安全的問題。
再比如,HashMap 一定是線程不安全的嗎?
說(shuō)不能說(shuō)的這么死吧。它是一個(gè)線程不安全的容器。但是如果我的使用場(chǎng)景是只讀呢?
在這個(gè)只讀的場(chǎng)景下,它就是線程安全的。
總之,看場(chǎng)景。道理,就是這么一個(gè)道理。
特別推薦一個(gè)分享架構(gòu)+算法的優(yōu)質(zhì)內(nèi)容,還沒關(guān)注的小伙伴,可以長(zhǎng)按關(guān)注一下:
長(zhǎng)按訂閱更多精彩▼
如有收獲,點(diǎn)個(gè)在看,誠(chéng)摯感謝
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問題,請(qǐng)聯(lián)系我們,謝謝!