談談ConcurrentHashMap1.7和1.8的不同實現
ConcurrentHashMap
在多執行緒環境下,使用HashMap
進行put
操作時存在丟失資料的情況,為了避免這種bug的隱患,強烈建議使用ConcurrentHashMap
代替HashMap
,為了對ConcurrentHashMap
有更深入的瞭解,本文將對ConcurrentHashMap
1.7和1.8的不同實現進行分析。
1.7實現
資料結構
jdk1.7中採用Segment
+ HashEntry
的方式進行實現,結構如下:
ConcurrentHashMap
初始化時,計算出Segment
陣列的大小ssize
和每個Segment
中HashEntry
陣列的大小cap
,並初始化Segment
ssize
大小為2的冪次方,預設為16,cap
大小也是2的冪次方,最小值為2,最終結果根據根據初始化容量initialCapacity
進行計算,計算過程如下:
1 2 3 4 5 |
|
其中Segment
在實現上繼承了ReentrantLock
,這樣就自帶了鎖的功能。
put實現
當執行put
方法插入資料時,根據key的hash值,在Segment
陣列中找到相應的位置,如果相應位置的Segment
還未初始化,則通過CAS進行賦值,接著執行Segment
物件的put
方法通過加鎖機制插入資料,實現如下:
場景:執行緒A和執行緒B同時執行相同Segment
物件的put
方法
1、執行緒A執行tryLock()
方法成功獲取鎖,則把HashEntry
物件插入到相應的位置;
2、執行緒B獲取鎖失敗,則執行scanAndLockForPut()
方法,在scanAndLockForPut
方法中,會通過重複執行tryLock()
方法嘗試獲取鎖,在多處理器環境下,重複次數為64,單處理器重複次數為1,當執行tryLock()
lock()
方法掛起執行緒B;
3、當執行緒A執行完插入操作時,會通過unlock()
方法釋放鎖,接著喚醒執行緒B繼續執行;
size實現
因為ConcurrentHashMap
是可以併發插入資料的,所以在準確計算元素時存在一定的難度,一般的思路是統計每個Segment
物件中的元素個數,然後進行累加,但是這種方式計算出來的結果並不一樣的準確的,因為在計算後面幾個Segment
的元素個數時,已經計算過的Segment
同時可能有資料的插入或則刪除,在1.7的實現中,採用瞭如下方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
先採用不加鎖的方式,連續計算元素的個數,最多計算3次:
1、如果前後兩次計算結果相同,則說明計算出來的元素個數是準確的;
2、如果前後兩次計算結果都不同,則給每個Segment
進行加鎖,再計算一次元素的個數;
1.8實現
資料結構
1.8中放棄了Segment
臃腫的設計,取而代之的是採用Node
+ CAS
+ Synchronized
來保證併發安全進行實現,結構如下:
只有在執行第一次put
方法時才會呼叫initTable()
初始化Node
陣列,實現如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
put實現
當執行put
方法插入資料時,根據key的hash值,在Node
陣列中找到相應的位置,實現如下:
1、如果相應位置的Node
還未初始化,則通過CAS插入相應的資料;
1 2 3 4 |
|
2、如果相應位置的Node
不為空,且當前該節點不處於移動狀態,則對該節點加synchronized
鎖,如果該節點的hash
不小於0,則遍歷連結串列更新節點或插入新節點;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
3、如果該節點是TreeBin
型別的節點,說明是紅黑樹結構,則通過putTreeVal
方法往紅黑樹中插入節點;
1 2 3 4 5 6 7 8 9 |
|
4、如果binCount
不為0,說明put
操作對資料產生了影響,如果當前連結串列的個數達到8個,則通過treeifyBin
方法轉化為紅黑樹,如果oldVal
不為空,說明是一次更新操作,沒有對元素個數產生影響,則直接返回舊值;
1 2 3 4 5 6 7 |
|
5、如果插入的是一個新節點,則執行addCount()
方法嘗試更新元素個數baseCount
;
size實現
1.8中使用一個volatile
型別的變數baseCount
記錄元素的個數,當插入新資料或則刪除資料時,會通過addCount()
方法更新baseCount
,實現如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
1、初始化時counterCells
為空,在併發量很高時,如果存在兩個執行緒同時執行CAS
修改baseCount
值,則失敗的執行緒會繼續執行方法體中的邏輯,使用CounterCell
記錄元素個數的變化;
2、如果CounterCell
陣列counterCells
為空,呼叫fullAddCount()
方法進行初始化,並插入對應的記錄數,通過CAS
設定cellsBusy欄位,只有設定成功的執行緒才能初始化CounterCell
陣列,實現如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
3、如果通過CAS
設定cellsBusy欄位失敗的話,則繼續嘗試通過CAS
修改baseCount
欄位,如果修改baseCount
欄位成功的話,就退出迴圈,否則繼續迴圈插入CounterCell
物件;
1 2 |
|
所以在1.8中的size
實現比1.7簡單多,因為元素個數儲存baseCount
中,部分元素的變化個數儲存在CounterCell
陣列中,實現如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
通過累加baseCount
和CounterCell
陣列中的數量,即可得到元素的總個數;