1.7和1.8中的ConCurrentHashMap對比
1.7中
數據結構
jdk1.7中採用Segment + HashEntry的方式進行實現,
初始化
ConcurrentHashMap初始化時,計算出Segment數組的大小ssize和每個Segment中HashEntry數組的大小cap,並初始化Segment數組的第一個元素;其中ssize大小爲2的冪次方,默認爲16,cap大小也是2的冪次方,最小值爲2,最終結果根據根據初始化容量initialCapacity進行計算。其中Segment在實現上繼承了ReentrantLock,這樣就自帶了鎖的功能。
Put實現
當執行put方法插入數據時,根據key的hash值,在Segment數組中找到相應的位置,如果相應位置的Segment還未初始化,則通過CAS進行賦值,接着執行Segment對象的put方法通過加鎖機制插入數據,實現如下:
場景:線程A和線程B同時執行相同Segment對象的put方法
線程A執行tryLock()方法成功獲取鎖,則把HashEntry對象插入到相應的位置;
線程B獲取鎖失敗,則執行scanAndLockForPut()方法,在scanAndLockForPut方法中,會通過重複執行tryLock()方法嘗試獲取鎖,在多處理器環境下,重複次數爲64,單處理器重複次數爲1,當執行tryLock()方法的次數超過上限時,則執行lock()方法掛起線程B;
當線程A執行完插入操作時,會通過unlock()方法釋放鎖,接着喚醒線程B繼續執行;
size實現
因爲ConcurrentHashMap是可以併發插入數據的,所以在準確計算元素時存在一定的難度,一般的思路是統計每個Segment對象中的元素個數,然後進行累加,但是這種方式計算出來的結果並不一樣的準確的,因爲在計算後面幾個Segment的元素個數時,已經計算過的Segment同時可能有數據的插入或則刪除,在1.7的實現中,採用瞭如下方式:
先採用不加鎖的方式,連續計算元素的個數,最多計算3次:
如果前後兩次計算結果相同,則說明計算出來的元素個數是準確的;
如果前後兩次計算結果都不同,則給每個Segment進行加鎖,再計算一次元素的個數;
1.8中
數據結構
1.8中放棄了Segment臃腫的設計,取而代之的是採用Node + CAS + Synchronized來保證併發安全進行實現。
只有在執行第一次put方法時纔會調用initTable()初始化Node數組。
put實現
當執行put方法插入數據時,根據key的hash值,在Node數組中找到相應的位置,實現如下:
1、如果相應位置的Node還未初始化,則通過CAS插入相應的數據;
2、如果相應位置的Node不爲空,且當前該節點不處於移動狀態,則對該節點加synchronized鎖,如果該節點的hash不小於0,則遍歷鏈表更新節點或插入新節點;
3、如果該節點是TreeBin類型的節點,說明是紅黑樹結構,則通過putTreeVal方法往紅黑樹中插入節點;
4、如果binCount不爲0,說明put操作對數據產生了影響,如果當前鏈表的個數達到8個,則通過treeifyBin方法轉化爲紅黑樹,如果oldVal不爲空,說明是一次更新操作,沒有對元素個數產生影響,則直接返回舊值;
5、如果插入的是一個新節點,則執行addCount()方法嘗試更新元素個數baseCount;
size實現
1.8中使用一個volatile類型的變量baseCount記錄元素的個數,當插入新數據或則刪除數據時,會通過addCount()方法更新baseCount。
1、初始化時counterCells爲空,在併發量很高時,如果存在兩個線程同時執行CAS修改baseCount值,則失敗的線程會繼續執行方法體中的邏輯,使用CounterCell記錄元素個數的變化;
2、如果CounterCell數組counterCells爲空,調用fullAddCount()方法進行初始化,並插入對應的記錄數,通過CAS設置cellsBusy字段,只有設置成功的線程才能初始化CounterCell數組。
3、如果通過CAS設置cellsBusy字段失敗的話,則繼續嘗試通過CAS修改baseCount字段,如果修改baseCount字段成功的話,就退出循環,否則繼續循環插入CounterCell對象;
所以在1.8中的size實現比1.7簡單多,因爲元素個數保存baseCount中,部分元素的變化個數保存在CounterCell數組中,通過累加baseCount和CounterCell數組中的數量,即可得到元素的總個數。