ConCurrentHashMap JDK1.7 和 JDK1.8 的區別
轉自:https://www.jianshu.com/p/933289f27270
ConCurrentHashMap 1.8 相比 1.7的話,主要改變為:
-
去除
Segment + HashEntry + Unsafe
的實現,
改為Synchronized + CAS + Node + Unsafe
的實現
其實 Node 和 HashEntry 的內容一樣,但是HashEntry是一個內部類。
用 Synchronized + CAS 代替 Segment ,這樣鎖的粒度更小了,並且不是每次都要加鎖了,CAS嘗試失敗了在加鎖。 -
put()方法中 初始化陣列大小時,1.8不用加鎖,因為用了個
sizeCtl
下面簡單介紹下主要的幾個方法的一些區別:
1. put() 方法
JDK1.7中的實現:
ConCurrentHashMap 和 HashMap 的put()方法實現基本類似,所以主要講一下為了實現併發性,ConCurrentHashMap 1.7 有了什麼改變
-
需要定位 2 次 (segments[i],segment中的table[i])
由於引入segment的概念,所以需要
- 先通過key的
rehash值的高位
和segments陣列大小-1
相與得到在 segments中的位置 - 然後在通過
key的rehash值
和table陣列大小-1
相與得到在table中的位置
-
沒獲取到 segment鎖的執行緒,沒有權力進行put操作,不是像HashTable一樣去掛起等待,而是會去做一下put操作前的準備:
- table[i]的位置(你的值要put到哪個桶中)
- 通過首節點first遍歷連結串列找有沒有相同key
- 在進行1、2的期間還不斷自旋獲取鎖,超過
64次
執行緒掛起!
JDK1.8中的實現:
- 先拿到根據
rehash值
定位,拿到table[i]的首節點first
,然後:
- 如果為
null
CAS
的方式把 value put進去 - 如果
非null
,並且first.hash == -1
,說明其他執行緒在擴容,參與一起擴容 - 如果
非null
,並且first.hash != -1
,Synchronized鎖住 first節點,判斷是連結串列還是紅黑樹,遍歷插入。
2. get() 方法
JDK1.7中的實現:
-
由於變數
value
是由volatile
修飾的,java記憶體模型中的happen before
規則保證了 對於 volatile 修飾的變數始終是寫操作
先於讀操作
的,並且還有 volatile 的記憶體可見性
保證修改完的資料可以馬上更新到主存中,所以能保證在併發情況下,讀出來的資料是最新的資料。 -
如果get()到的是null值才去加鎖。
JDK1.8中的實現:
- 和 JDK1.7類似
3. resize() 方法
JDK1.7中的實現:
- 跟HashMap的 resize() 沒太大區別,都是在 put() 元素時去做的擴容,所以在1.7中的實現是獲得了鎖之後,在單執行緒中去做擴容(1.
new個2倍陣列
2.遍歷old陣列節點搬去新陣列
)。
JDK1.8中的實現:
- jdk1.8的擴容支援併發遷移節點,從old陣列的尾部開始,如果該桶被其他執行緒處理過了,就建立一個 ForwardingNode 放到該桶的首節點,hash值為-1,其他執行緒判斷hash值為-1後就知道該桶被處理過了。
4. 計算size
JDK1.7中的實現:
- 先採用不加鎖的方式,計算兩次,如果兩次結果一樣,說明是正確的,返回。
- 如果兩次結果不一樣,則把所有 segment 鎖住,重新計算所有 segment的
Count
的和
JDK1.8中的實現:
由於沒有segment的概念,所以只需要用一個 baseCount
變數來記錄ConcurrentHashMap 當前 節點的個數
。
- 先嚐試通過CAS 修改
baseCount
- 如果多執行緒競爭激烈,某些執行緒CAS失敗,那就CAS嘗試將
CELLSBUSY
置1,成功則可以把baseCount變化的次數
暫存到一個數組counterCells
裡,後續陣列counterCells
的值會加到baseCount
中。 - 如果
CELLSBUSY
置1失敗又會反覆進行CASbaseCount
和 CAScounterCells
陣列