為什么需要 ConcurrentHashMap
Java 早期的同步類 HashTable 和 Collections 提供的同步包裝器為我們提供了線程安全的容器踱蠢,但是因為這兩種方式都是在整個大操作 put障本、get、size 級別上加鎖實現(xiàn)的癣猾。并發(fā)效率很低。作為改進余爆,Java 提供了更為高效的并發(fā)容器——ConcurrentHashMap纷宇。
jdk 1.7 ConcurrentHashMap 分析
ConcurrentHashMap 的設計其實一直在演化,早期的 ConcurrentHashMap蛾方,其實現(xiàn)是基于:
- 分離鎖像捶,也就是將內(nèi)部進行分段(Segment)上陕,里面則是 HashEntry 的數(shù)組,和 HashMap 類似拓春,哈希相同的條目也是以鏈表的形式存放释簿。
- HashEntry 內(nèi)部使用 volatile 的 value 字段來保證可見性,也利用了不可變對象的機制以改進利用 Unsafe 提供的底層能力硼莽,比如 volatile access庶溶,去直接完成部分操作,以最優(yōu)化性能懂鸵,畢竟 Unsafe 中的很多操作都是 JVM intrinsic 優(yōu)化過的偏螺。
這個版本的核心是利用分段設計,在進行并發(fā)操作時匆光,鎖住相應的 Segment套像,來避免鎖住整個表,大大提高了性能终息。
構(gòu)造時夺巩,Segment 的數(shù)量由所謂的concurrentcyLevel 決定,默認是 16周崭,可以通過構(gòu)造函數(shù)顯式指定柳譬,這個輸入的值會被 Java 自動調(diào)整為最近的 2 的冪數(shù)值,比如輸入 15续镇,就會被自動調(diào)整為 16征绎。之所以這樣調(diào)整,是因為 2 的冪次方減一就是一個二進制低位全是 1 的“掩碼”磨取,在確定元素在哪個 Segment 時會利用到人柿。
在進行并發(fā)寫操作時:
- ConcurrentHashMap 會獲取再入鎖,以保證數(shù)據(jù)一致性忙厌,Segment 本身就是基于 ReentrantLock 的擴展實現(xiàn)的凫岖,所以,在并發(fā)修改期間逢净,相應的 Segment 是被鎖定的哥放。
- 在最初階段,進行重復性的掃描爹土,以確定相應的 key 的值是否已經(jīng)在數(shù)組里面甥雕,進而決定是更新還是放置操作。重復掃描胀茵、檢測沖突是 ConcurrentHashMap 的常見技巧社露。
另一個需要關(guān)注的點是 size 方法,它的實現(xiàn)涉及分離鎖的一個副作用琼娘。
如果不加鎖直接計算所有 Segment 的總值峭弟,在并發(fā) put 的情況下附鸽,就可能導致結(jié)果不準確,但是直接鎖定所有 Segment 進行計算瞒瘸,代價就很昂貴坷备。其實,分離鎖也限制了 Map 的初始化等操作情臭。
這個版本的 ConcurrentHashMap 是通過重試機制(RETRIES_BEFORE_LOCK省撑,指定重試次數(shù) 2),來試圖獲得可靠值俯在。如果沒有監(jiān)控到發(fā)生變化(通過對比 Segment.modCount)竟秫,就直接返回,否則獲取鎖進行操作朝巫。
jdk 1.8 ConcurrentHashMap 分析
- 總體結(jié)構(gòu)上鸿摇,它的內(nèi)部存儲結(jié)構(gòu)變得和 HashMap 相似石景,同樣是大的桶(bucket)數(shù)組劈猿,然后內(nèi)部也是一個個所謂的鏈表結(jié)構(gòu)(bin),同步的力度要更細致一些潮孽。在單個桶中數(shù)據(jù)過多時揪荣,會進行樹化。
- 其內(nèi)部保留了 Segment 定義往史,目的是為了保證序列化時的兼容性仗颈,不再有任何結(jié)構(gòu)上的用處。
- 因為不再使用 Segment椎例,初始化操作大大簡化挨决,修改為 lazy-load 形式,有效避免了初始開銷订歪,解決了老版本很多人抱怨的這一點脖祈。
- 數(shù)據(jù)存儲利用 volatile 來保證可見性。
- 使用 CAS 等操作刷晋,在特定場景進行無鎖并發(fā)操作盖高。
- 使用 Unsafe、LongAdder 之類的底層手段眼虱,進行極端情況的優(yōu)化喻奥。