做者:炸雞可樂
原文出處:www.pzblog.cnhtml
在以前的集合文章中,咱們瞭解到 HashMap 在多線程環境下操做可能會致使程序死循環的線上故障!java
既然在多線程環境下不能使用 HashMap,那若是咱們想在多線程環境下操做 map,該怎麼操做呢?node
想必閱讀太小編以前寫的《HashMap 在多線程環境下操做可能會致使程序死循環》一文的朋友們必定知道,其中有一個解決辦法就是使用 java 併發包下的 ConcurrentHashMap 類!git
今天呢,咱們就一塊兒來聊聊 ConcurrentHashMap 這個類!數組
衆所周知,在 Java 中,HashMap 是非線程安全的,若是想在多線程下安全的操做 map,主要有如下解決方法:安全
Hashtable
線程安全類;Collections.synchronizedMap
方法,對方法進行加同步鎖;ConcurrentHashMap
類;在以前的文章中,關於 Hashtable 類,咱們也有所介紹,Hashtable 是一個線程安全的類,Hashtable 幾乎全部的添加、刪除、查詢方法都加了synchronized
同步鎖!數據結構
至關於給整個哈希表加了一把大鎖,多線程訪問時候,只要有一個線程訪問或操做該對象,那其餘線程只能阻塞等待須要的鎖被釋放,在競爭激烈的多線程場景中性能就會很是差,因此 Hashtable 不推薦使用!多線程
再來看看第二種方法,使用Collections.synchronizedMap
方法,咱們打開 JDK 源碼,部份內容以下:併發
能夠很清晰的看到,若是傳入的是 HashMap 對象,其實也是對 HashMap 作的方法作了一層包裝,裏面使用對象鎖來保證多線程場景下,操做安全,本質也是對 HashMap 進行全表鎖!ide
使用Collections.synchronizedMap
方法,在競爭激烈的多線程環境下性能依然也很是差,因此不推薦使用!
上面2種方法,因爲都是對方法進行全表鎖,因此在多線程環境下容易形成性能差的問題,由於** hashMap 是數組 + 鏈表的數據結構,若是咱們把數組進行分割多段,對每一段分別設計一把同步鎖,這樣在多線程訪問不一樣段的數據時,就不會存在鎖競爭了,這樣是否是能夠有效的提升性能?**
再來看看第三種方法,使用併發包中的ConcurrentHashMap
類!
ConcurrentHashMap 類所採用的正是分段鎖的思想,將 HashMap 進行切割,把 HashMap 中的哈希數組切分紅小數組,每一個小數組有 n 個 HashEntry 組成,其中小數組繼承自ReentrantLock(可重入鎖)
,這個小數組名叫Segment
, 以下圖:
固然,JDK1.7 和 JDK1.8 對 ConcurrentHashMap 的實現有很大的不一樣!
JDK1.8 對 HashMap 作了改造,當衝突鏈表長度大於8時,會將鏈表轉變成紅黑樹結構,上圖是 ConcurrentHashMap 的總體結構,參考 JDK1.7!
咱們再來看看 JDK1.8 中 ConcurrentHashMap 的總體結構,內容以下:
JDK1.8 中 ConcurrentHashMap 類取消了 Segment 分段鎖,採用 CAS
+ synchronized
來保證併發安全,數據結構跟 jdk1.8 中 HashMap 結構相似,都是數組 + 鏈表(當鏈表長度大於8時,鏈表結構轉爲紅黑二叉樹)結構。
ConcurrentHashMap 中 synchronized 只鎖定當前鏈表或紅黑二叉樹的首節點,只要節點 hash 不衝突,就不會產生併發,相比 JDK1.7 的 ConcurrentHashMap 效率又提高了 N 倍!
說了這麼多,咱們再一塊兒來看看 ConcurrentHashMap 的源碼實現。
JDK 1.7 的 ConcurrentHashMap 採用了很是精妙的分段鎖策略,打開源碼,能夠看到 ConcurrentHashMap 的主存是一個 Segment 數組。
咱們再來看看 Segment 這個類,在 ConcurrentHashMap 中它是一個靜態內部類,內部結構跟 HashMap 差很少,源碼以下:
存放元素的 HashEntry,也是一個靜態內部類,源碼以下:
HashEntry
和HashMap
中的 Entry
很是相似,惟一的區別就是其中的核心數據如value
,以及next
都使用了volatile
關鍵字修飾,保證了多線程環境下數據獲取時的可見性!
從類的定義上能夠看到,Segment 這個靜態內部類繼承了ReentrantLock
類,ReentrantLock
是一個可重入鎖,若是瞭解過多線程的朋友們,對它必定不陌生。
ReentrantLock
和synchronized
均可以實現對線程進行加鎖,不一樣點是:ReentrantLock
能夠指定鎖是公平鎖仍是非公平鎖,操做上也更加靈活,關於此類,具體在之後的多線程篇幅中會單獨介紹。
由於ConcurrentHashMap
的大致存儲結構和HashMap
相似,因此就不對每一個方法進行單獨分析介紹了,關於HashMap
的分析,有興趣的朋友能夠參閱小編以前寫的《深刻分析 HashMap》一文。
ConcurrentHashMap 在存儲方面是一個 Segment 數組,一個 Segment 就是一個子哈希表,Segment 裏維護了一個 HashEntry 數組,其中 Segment 繼承自 ReentrantLock,併發環境下,對於不一樣的 Segment 數據進行操做是不用考慮鎖競爭的,所以不會像 Hashtable 那樣無論是添加、刪除、查詢操做都須要同步處理。
理論上 ConcurrentHashMap 支持 concurrentLevel(經過 Segment 數組長度計算得來) 個線程併發操做,每當一個線程獨佔一把鎖訪問 Segment 時,不會影響到其餘的 Segment 操做,效率大大提高!
上面介紹完了對象屬性,咱們繼續來看看 ConcurrentHashMap 的構造方法,源碼以下:
this
調用對應的構造方法,源碼以下:
從源碼上能夠看出,ConcurrentHashMap 初始化方法有三個參數,initialCapacity(初始化容量)爲1六、loadFactor(負載因子)爲0.7五、concurrentLevel(併發等級)爲16,若是不指定則會使用默認值。
其中,值得注意的是 concurrentLevel 這個參數,雖然 Segment 數組大小 ssize 是由 concurrentLevel 來決定的,可是卻不必定等於 concurrentLevel,ssize 經過位移動運算,必定是大於或者等於 concurrentLevel 的最小的 2 的次冪!
經過計算能夠看出,按默認的 initialCapacity 初始容量爲16,concurrentLevel 併發等級爲16,理論上就容許 16 個線程併發執行,而且每個線程獨佔一把鎖訪問 Segment,不影響其它的 Segment 操做!
從以前的文章中,咱們瞭解到 HashMap 在多線程環境下操做可能會致使程序死循環,仔細想一想你會發現,形成這個問題無非是 put 和擴容階段發生的!
那麼這樣咱們就能夠從 put 方法下手了,來看看 ConcurrentHashMap 是怎麼操做的?
ConcurrentHashMap 的 put 方法,源碼以下:
從源碼能夠看出,這部分的 put 操做主要分兩步:
真正插入元素的 put 方法,源碼以下:
從源碼能夠看出,真正的 put 操做主要分如下幾步:
scanAndLockForPut
方法,這個方法也是嘗試獲取對象鎖;咱們再來看看,上面提到的scanAndLockForPut
這個方法,源碼以下:
scanAndLockForPut
這個方法,操做也是分如下幾步:
lock()
方法獲取對象鎖,若是依然沒有獲取到,當前線程就阻塞,直到獲取以後退出循環;經過scanAndLockForPut()
方法,當前線程就能夠在即便獲取不到segment
鎖的狀況下,完成須要添加節點的實例化工做,當獲取鎖後,就能夠直接將該節點插入鏈表便可。
這個方法還實現了相似於自旋鎖的功能,循環式的判斷對象鎖是否可以被成功獲取,直到獲取到鎖纔會退出循環,防止執行 put 操做的線程頻繁阻塞,這些優化都提高了 put 操做的性能。
get 方法就比較簡單了,由於不涉及增、刪、改操做,因此不存在併發故障問題,源碼以下:
因爲 HashEntry 涉及到的共享變量都使用 volatile 修飾,volatile 能夠保證內存可見性,因此不會讀取到過時數據。
remove 操做和 put 方法差很少,都須要獲取對象鎖才能操做,經過 key 找到元素所在的 Segment 對象而後移除,源碼以下:
與 get 方法相似,先獲取 Segment 數組所在的 Segment 對象,而後經過 Segment 對象去移除元素,源碼以下:
先獲取對象鎖,若是獲取到以後執行移除操做,以後的操做相似 hashMap 的移除方法,步驟以下:
雖然 JDK1.7 中的 ConcurrentHashMap 解決了 HashMap 併發的安全性,可是當衝突的鏈表過長時,在查詢遍歷的時候依然很慢!
在 JDK1.8 中,HashMap 引入了紅黑二叉樹設計,當衝突的鏈表長度大於8時,會將鏈表轉化成紅黑二叉樹結構,紅黑二叉樹又被稱爲平衡二叉樹,在查詢效率方面,又大大的提升了很多。
由於 HashMap 並不支持在多線程環境下使用, JDK1.8 中的ConcurrentHashMap 和往期 JDK 中的 ConcurrentHashMa 同樣支持併發操做,總體結構和 JDK1.8 中的 HashMap 相似,相比 JDK1.7 中的 ConcurrentHashMap, 它拋棄了原有的 Segment 分段鎖實現,採用了 CAS + synchronized
來保證併發的安全性。
JDK1.8 中的 ConcurrentHashMap 對節點Node
類中的共享變量,和 JDK1.7 同樣,使用volatile
關鍵字,保證多線程操做時,變量的可見行!
其餘的細節,與 JDK1.8 中的 HashMap 相似,咱們來具體看看 put 方法!
打開 JDK1.8 中的 ConcurrentHashMap 中的 put 方法,源碼以下:
當進行 put 操做時,流程大概能夠分以下幾個步驟:
f
,在當前數組下標是否第一次插入,若是是就經過 CAS 方式插入;f.hash == -1
是否成立,若是成立,說明當前f
是ForwardingNode
節點,表示有其它線程正在擴容,則一塊兒進行擴容操做;Node
節點按鏈表或紅黑樹的方式插入到合適的位置;8
,若是超過8
個,就將鏈表轉化爲紅黑樹結構;put 操做大體的流程,就是這樣的,能夠看的出,複雜程度比 JDK1.7 上了一個臺階。
咱們再來看看源碼中的第3步 initTable()
方法,若是數組爲空就初始化數組,源碼以下:
sizeCtl 是一個對象屬性,使用了volatile關鍵字修飾保證併發的可見性,默認爲 0,當第一次執行 put 操做時,經過Unsafe.compareAndSwapInt()
方法,俗稱CAS
,將 sizeCtl
修改成 -1
,有且只有一個線程可以修改爲功,接着執行 table 初始化任務。
若是別的線程發現sizeCtl<0
,意味着有另外的線程執行CAS操做成功,當前線程經過執行Thread.yield()
讓出 CPU 時間片等待 table 初始化完成。
咱們繼續來看看 put 方法中第5步helpTransfer()
方法,若是f.hash == -1
成立,說明當前f
是ForwardingNode
節點,意味有其它線程正在擴容,則一塊兒進行擴容操做,源碼以下:
這個過程,操做步驟以下:
sizeCtl < 0
,說明還在擴容;sizeCtl + 1
, 增長了一個線程幫助其擴容;咱們再來看看源碼中的第9步 addCount()
方法,插入完成以後,擴容判斷,源碼以下:
這個過程,操做步驟以下:
put 的流程基本分析完了,能夠從中發現,裏面大量的使用了CAS
方法,CAS 表示比較與替換,裏面有3個參數,分別是目標內存地址、舊值、新值,每次判斷的時候,會將舊值與目標內存地址中的值進行比較,若是相等,就將新值更新到內存地址裏,若是不相等,就繼續循環,直到操做成功爲止!
雖然使用的了CAS
這種樂觀鎖方法,可是裏面的細節設計的很複雜,閱讀比較費神,有興趣的朋友們能夠本身研究一下。
get 方法操做就比較簡單了,由於不涉及併發操做,直接查詢就能夠了,源碼以下:
從源碼中能夠看出,步驟以下:
reomve 方法操做和 put 相似,只是方向是反的,源碼以下:
replaceNode 方法,源碼以下:
從源碼中能夠看出,步驟以下:
check= -1
,因此不會進行擴容操做,利用CAS操做修改baseCount值;雖然 HashMap 在多線程環境下操做不安全,可是在 java.util.concurrent
包下,java 爲咱們提供了 ConcurrentHashMap 類,保證在多線程下 HashMap 操做安全!
在 JDK1.7 中,ConcurrentHashMap 採用了分段鎖策略,將一個 HashMap 切割成 Segment 數組,其中 Segment 能夠當作一個 HashMap, 不一樣點是 Segment 繼承自 ReentrantLock,在操做的時候給 Segment 賦予了一個對象鎖,從而保證多線程環境下併發操做安全。
可是 JDK1.7 中,HashMap 容易由於衝突鏈表過長,形成查詢效率低,因此在 JDK1.8 中,HashMap 引入了紅黑樹特性,當衝突鏈表長度大於8時,會將鏈表轉化成紅黑二叉樹結構。
在 JDK1.8 中,與此對應的 ConcurrentHashMap 也是採用了與 HashMap 相似的存儲結構,可是 JDK1.8 中 ConcurrentHashMap 並無採用分段鎖的策略,而是在元素的節點上採用 CAS + synchronized
操做來保證併發的安全性,源碼的實現比 JDK1.7 要複雜的多。
本文由於是斷斷續續寫出來,若是有理解不對的地方,歡迎各位網友指出!
一、JDK1.7&JDK1.8 源碼
二、JavaGuide - 容器 - ConcurrentHashMap