ConCurrentHashMap 1.8 相比 1.7的話,主要改變為:
-
去除
Segment + HashEntry + Unsafe
的實現,
改為Synchronized + CAS + Node + Unsafe
的實現
其實 Node 和 HashEntry 的內容一樣,但是HashEntry是一個內部類。
用 Synchronized + CAS 代替 Segment ,這樣鎖的粒度更小了,並且不是每次都要加鎖了,CAS嘗試失敗了在加鎖。 -
put()方法中 初始化數組大小時,1.8不用加鎖,因為用了個
sizeCtl
變量,將這個變量置為-1,就表明table正在初始化。
下面簡單介紹下主要的幾個方法的一些區別:
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中的位置
- 先通過key的
-
沒獲取到 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
的和
- 如果兩次結果不一樣,則把所有 segment 鎖住,重新計算所有 segment的
JDK1.8中的實現:
由於沒有segment的概念,所以只需要用一個 baseCount
變量來記錄ConcurrentHashMap 當前 節點的個數
。
-
- 先嘗試通過CAS 修改
baseCount
- 先嘗試通過CAS 修改
-
- 如果多線程競爭激烈,某些線程CAS失敗,那就CAS嘗試將
CELLSBUSY
置1,成功則可以把baseCount變化的次數
暫存到一個數組counterCells
里,后續數組counterCells
的值會加到baseCount
中。
- 如果多線程競爭激烈,某些線程CAS失敗,那就CAS嘗試將
-
- 如果
CELLSBUSY
置1失敗又會反復進行CASbaseCount
和 CAScounterCells
數組
- 如果