Jdk1.6和1.7版本中ConcurrentHashMap的弱一致性

前言:
昨天看技術(shù)論壇時(shí)看到了一篇<<為什么ConcurrentHashMap是弱一致的>>文章,其中的弱一致性是基于Jdk1.6版本的ConcurrentHashMap源碼分析的,而在Jdk1.7和Jdk1.8中ConcurrentHashMap的設(shè)計(jì)都進(jìn)行了一定程度的改良,是否仍然存在1.6版本中的弱一致性呢?
本文為了分析方便,假設(shè)有兩個(gè)線程對(duì)一個(gè)ConcurrentHashMap實(shí)例進(jìn)行并發(fā)處理,采用put-get這一并發(fā)操作組合來分析數(shù)據(jù)的一致性,put-get組合表示一個(gè)線程執(zhí)行put方法時(shí)另一個(gè)線程執(zhí)行g(shù)et方法。另外,文章從1.6版本和1.7版本兩個(gè)維度同時(shí)分析同一組合操作時(shí)的現(xiàn)象,進(jìn)一步驗(yàn)證結(jié)論。
閱讀此文章讀者可能需要Java內(nèi)存模型(JMM)、volatile內(nèi)存語義、Happens Before關(guān)系、CAS、AQS、ConcurrentHashMap不同版本實(shí)現(xiàn)原理等基礎(chǔ)前驅(qū)知識(shí),文章只對(duì)一致性問題進(jìn)行分析

在分析之前首先要知道ConcurrentHashMap在1.6和1.7中用volatile修飾的變量有哪些,如下表所示

1.6 1.7
Segment.count Segment.HashEntry[]
Segment.HashEntry[] Segment.HashEntry.value
Segment.HashEntry.value Segment.HashEntry.next

眾所周知,ConcurrentHashMap使用鎖分離技術(shù),初始時(shí)有16個(gè)Segment段組成,一個(gè)Segment段包含一個(gè)類型為HashEntry的數(shù)組table,一個(gè)HashEntry元素又存在key-value的鍵值對(duì),以及指向下一個(gè)HashEntry元素的指針next
所以,表中的Segment.count表示每個(gè)Segment段中HashEntry[]內(nèi)元素的數(shù)量;Segment.HashEntry[]表示Segment段內(nèi)的HashEntry[]數(shù)組;Segment.HashEntry.value表示一個(gè)HashEntry元素中的value值;Segment.HashEntry.next表示HashEntry鏈表中下一個(gè)元素

圖1. 1.6版本put方法
圖2. 1.6版本get方法

因?yàn)?code>count被volatile修飾,因此標(biāo)注1是對(duì)volatile的讀操作,同理標(biāo)注2也是對(duì)volatile修飾的table(HashEntry[])的讀操作;根據(jù)Happens Before規(guī)則中的程序順序規(guī)則(一個(gè)線程中的每個(gè)操作,happens-before于該線程中的任意后續(xù)操作),可以看出 1 happens before 2 happens before 3 happens before 4。其中,標(biāo)注3是對(duì)普通變量的普通寫操作,標(biāo)注4是對(duì)volatile變量count的寫操作;同樣的道理在圖2中存在5 happens before 6
那么我們模擬一下兩個(gè)線程分別操作put方法和get方法的情況,首先是“正?!钡那闆r

圖3. 1.6版本下數(shù)據(jù)一致性正常的情況

圖中因?yàn)闃?biāo)注1、3、4在同一線程中,因此存在happens before關(guān)系,另一個(gè)線程的5和6也存在happens before關(guān)系;此外,根據(jù)Happens Before規(guī)則中的volatile變量規(guī)則(對(duì)一個(gè)volatile域的寫,happens-before于任意后續(xù)對(duì)這個(gè)volatile域的讀)可以推理出4 happens before 5,因?yàn)?是對(duì)volatile變量count的寫操作,5是對(duì)count的讀操作。在根據(jù)Happens Before規(guī)則中的傳遞性(如果A happens-before B,且B happens-before C,那么A happens-before C),最終推出1 happens before 2 happens before 3 happens before 4 happens before 5 happens before 6,因此此時(shí)數(shù)據(jù)的一致性是得到保證的。那么我們?cè)賮砜纯慈跻恢碌那闆r

圖4. 1.6版本下數(shù)據(jù)弱一致性的情況

我們更改了4、5執(zhí)行的順序,一個(gè)線程先執(zhí)行對(duì)volatile變量count的讀操作,之后另一個(gè)線程再執(zhí)行對(duì)count的寫操作,這樣4、5之間就不存在happens before關(guān)系了,并且上面分析過3的操作只是普通變量的讀寫操作,而5是對(duì)volatile變量table的讀操作,因此3、5之間也不存在happens before關(guān)系,6中讀取的并不是3處添加新HashEntry的最新table,這就導(dǎo)致了數(shù)據(jù)的弱一致性
下面我們來看看在Jdk1.7中ConcurrentHashMap的put和get方法

圖5. 1.7版本put方法
圖6. 1.7版本get方法

因?yàn)閠able為volatile修飾,因此標(biāo)注1是一個(gè)volatile讀,用一個(gè)局部非volatile修飾引用了volatile修飾的volatile,這里必須要注意一個(gè)知識(shí)點(diǎn):volatile修飾的是reference,不是對(duì)象的實(shí)例,也就是說table指向了一個(gè)堆內(nèi)內(nèi)容為HashEntry[]內(nèi)容的空間,這里的volatile修飾的是這個(gè)table在棧內(nèi)的引用,不是棧內(nèi)地址指向的堆內(nèi)內(nèi)容,而HashEntry<K,V>[] tab = table相當(dāng)于又用了另一個(gè)變量tab指向了變量table指向的同一塊內(nèi)存地址,但是tab引用并沒有被volatile修飾,所以tab是不具有volatile語義的相關(guān)特性的
標(biāo)注2調(diào)用了HashEntry<K,V> entryAt(HashEntry<K,V>[] tab, int i)方法,源碼如下:

圖7. entryAt方法

entryAt方法調(diào)用了UNSAFE類的getObjectVolatile,該方法是根據(jù)第二個(gè)參數(shù)的offset偏移量查找對(duì)應(yīng)第一個(gè)參數(shù)中的值,該方法是支持volatile讀語義的,其底層是在entryAt方法插入一個(gè)LoadLoad和一個(gè)LoadStore內(nèi)存屏障防止CPU可能的指令重排序
接著再回去看圖5中的標(biāo)注3處,將新加入的HashEntry實(shí)例放在HashEntry[]特定的位置上,下面是其源碼

圖7 setEntryAt方法

同樣的setEntryAt方法內(nèi)部也調(diào)用了UNSAFE類的putOrderedObject方法,這里又存在一個(gè)坑,很多文章在分析該方法時(shí)都說其是一個(gè)具有volatile語義的方法,或者是否具有volatile語義依賴于第一個(gè)參數(shù)是否是volatile變量,但實(shí)際上putOrderedObject并不具有volatile語義,該方法的底層省去了volatile寫的StoreLoad內(nèi)存屏障,只添加了StoreStore內(nèi)存屏障,所以只能保證putOrderedObject方法之前的內(nèi)存可見性,不能保證數(shù)據(jù)的一致性,讀者可以參考JUC中Atomic class之lazySet的一點(diǎn)疑惑,對(duì)該問題分析的非常漂亮,源碼扒到了祖墳上
根據(jù)Happens Before規(guī)則中的程序順序規(guī)則可以得出1 happens before 2 happens before 3,同理推出圖6中4 happens before 5 happens before 6,但是對(duì)比圖5和圖6的代碼發(fā)現(xiàn),圖5中和圖6中只有table一個(gè)被volatile修飾的變量被共享,而且在put方法中table是volatile讀,get方法中table也是volatile讀,按照Happens Before規(guī)則中的volatile變量規(guī)則,必須存在另一個(gè)volatile的寫,在這里也就是對(duì)于table變量的寫,且寫要在讀之前才會(huì)行成Happens Before關(guān)系,很明顯也不滿足
我們?cè)賹?duì)比1.6版本中是如何完成“部分?jǐn)?shù)據(jù)一致性”的,在1.6中count變量被volatile修飾了,因此該變量可以作為兩個(gè)線程發(fā)生volatile的媒介,但在1.7版本中,count變量沒有被volatile修飾,因此也不存在依靠該變量發(fā)生Happens Before關(guān)系的可能性。put方法和get方法中都存在對(duì)于局部變量tab的volatile操作,但經(jīng)過逃逸性分析,這里的局部變量并不會(huì)逃逸到另一個(gè)線程中,所以也不會(huì)存在Happens Before語義
最后只剩標(biāo)注4中的UNSAFE.getObjectVolatile(segments, u),上面分析過,雖然參數(shù)中的segments沒有被volatile修飾,但是getObjectVolatile會(huì)強(qiáng)制在變量讀取之后加上LoadLoadLoadStore內(nèi)存屏障行成volatile讀語義,但在put方法時(shí)也不存在對(duì)于該共享變量的volatile寫操作,也就更談不上行成Happens Before關(guān)系了。因此1.7版本ConcurrentHashMap的數(shù)據(jù)弱一致性也得以論證

后記
在寫這篇文章的過程中發(fā)現(xiàn)很多之前“掌握”的知識(shí)其實(shí)非常膚淺,迫使我查閱各種資料和規(guī)范,也得到了很多大牛的幫助,即便寫完文章也感覺有各種理解的不恰當(dāng)?shù)牡胤?,摒棄浮躁,學(xué)無止境,永遠(yuǎn)以學(xué)生的心態(tài)腳踏實(shí)地前進(jìn)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容