前言:
昨天看技術(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ā)操作組合來(lái)分析數(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)存語(yǔ)義晒喷、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è)元素
因?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方法的情況者春,首先是“正称葡危”的情況
圖中因?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è)賮?lái)看看弱一致的情況
我們更改了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ù)的弱一致性
下面我們來(lái)看看在Jdk1.7中ConcurrentHashMap
的put和get方法
因?yàn)閠able為volatile修飾踢京,因此標(biāo)注1是一個(gè)volatile讀,用一個(gè)局部非volatile修飾引用了volatile修飾的volatile宦棺,這里必須要注意一個(gè)知識(shí)點(diǎn):volatile修飾的是reference瓣距,不是對(duì)象的實(shí)例,也就是說(shuō)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語(yǔ)義的相關(guān)特性的
標(biāo)注2調(diào)用了HashEntry<K,V> entryAt(HashEntry<K,V>[] tab, int i)
方法,源碼如下:
entryAt
方法調(diào)用了UNSAFE
類的getObjectVolatile
思瘟,該方法是根據(jù)第二個(gè)參數(shù)的offset偏移量查找對(duì)應(yīng)第一個(gè)參數(shù)中的值荸百,該方法是支持volatile讀語(yǔ)義的,其底層是在entryAt
方法插入一個(gè)LoadLoad
和一個(gè)LoadStore
內(nèi)存屏障防止CPU可能的指令重排序接著再回去看圖5中的標(biāo)注3處滨攻,將新加入的
HashEntry
實(shí)例放在HashEntry[]
特定的位置上够话,下面是其源碼
同樣的
setEntryAt
方法內(nèi)部也調(diào)用了UNSAFE
類的putOrderedObject
方法,這里又存在一個(gè)坑光绕,很多文章在分析該方法時(shí)都說(shuō)其是一個(gè)具有volatile語(yǔ)義的方法女嘲,或者是否具有volatile語(yǔ)義依賴于第一個(gè)參數(shù)是否是volatile變量,但實(shí)際上putOrderedObject
并不具有volatile語(yǔ)義诞帐,該方法的底層省去了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語(yǔ)義最后只剩標(biāo)注4中的
UNSAFE.getObjectVolatile(segments, u)
座云,上面分析過,雖然參數(shù)中的segments
沒有被volatile修飾付材,但是getObjectVolatile
會(huì)強(qiáng)制在變量讀取之后加上LoadLoad
和LoadStore
內(nèi)存屏障行成volatile讀語(yǔ)義朦拖,但在put方法時(shí)也不存在對(duì)于該共享變量的volatile寫操作,也就更談不上行成Happens Before關(guān)系了伞租。因此1.7版本ConcurrentHashMap
的數(shù)據(jù)弱一致性也得以論證
后記
在寫這篇文章的過程中發(fā)現(xiàn)很多之前“掌握”的知識(shí)其實(shí)非常膚淺贞谓,迫使我查閱各種資料和規(guī)范,也得到了很多大牛的幫助葵诈,即便寫完文章也感覺有各種理解的不恰當(dāng)?shù)牡胤铰阆遥饤壐≡辏瑢W(xué)無(wú)止境作喘,永遠(yuǎn)以學(xué)生的心態(tài)腳踏實(shí)地前進(jìn)