基於jdk1.8
ConcurrentHash擴容-數據遷移階段
- 擴容階段源碼註釋版本
// 對目標節點位置加鎖,開始處理數據
synchronized (f) {
// 雙重校驗
if (tabAt(tab, i) == f) {
// ln:低位節點 hn:高位節點
Node<K,V> ln, hn;
if (fh >= 0) {
// fh:當前節點hash值 n:原數組長度
// 計算runBit的有兩種情況 1.等於零 2.不等於零
int runBit = fh & n;
Node<K,V> lastRun = f;
for (Node<K,V> p = f.next; p != null; p = p.next) {
// 循環遍歷鏈表中的元素,對鏈表中的元素進行分類
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
// 拼接兩條鏈,即把鏈表進行拆分 構造高位和低位鏈表
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
// 低位鏈保持位置不動
setTabAt(nextTab, i, ln);
// 高位鏈路移動到原位置加n位置 例如對位置爲10的鏈表進行遷移,高位鏈遷移後的位置爲26
setTabAt(nextTab, i + n, hn);
// 將處理過的位置放置fwd節點,表示該位置已經被處理過了
setTabAt(tab, i, fwd);
advance = true;
}
else if (f instanceof TreeBin) {
// ....紅黑樹邏輯實現
}
}
}
ConcurrentHashMap—精華提煉
- 切入點
- 通過 https://blog.csdn.net/GoNewWay/article/details/105346064 的分析,可以瞭解到學習ConcurrentHashMap的切入點在put方法
- 精華提煉
Question—HashMap爲什麼要進行hash再運算?
Answer
ConcurrentHashMap中調用put方法,通過spread方法對元素的hashCode值做再一次運算。元素的hash值通過該運算計算過後,最高位一定爲0。把計算的結果控制在int最大整數之內。
// h >>> 16 將元素的hash值無符號右移16位,即高位全是零
// h ^ (h >>> 16) 做異或運算
// & HASH_BITS 即 & 0x7fffffff 做“位與”運算
// 0x7fffffff 表示int類型的最大整型數 即在2進制中除了首位都是1 進行 & 運算後,最高位必爲0
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
爲了便於理解,做一個僞運算。令 h 爲 1111 1101 1111 1010 0010 1100 1011 0010
1111 1101 1111 1010 0010 1100 1011 0010
0000 0000 0000 0000 1111 1101 1111 1010 // h >>> 16
1111 1101 1111 1010 1101 0001 0100 1000 // h ^ (h >>> 16)
0111 1111 1111 1111 1111 1111 1111 1111 // 0x7fffffff
// 最終結果
0111 1101 1111 1010 1101 0001 0100 1000 // (h ^ (h >>> 16)) & HASH_BITS
這樣做的目的主要是混合了元素的 高16位和低16位,最終計算得到的結果具備了元素原高位和低位的特徵。使hash值更加不確定來降低碰撞的概率。該算法又被成爲 擾動函數/擾動算法。
Question—ConcurrentHashMap什麼時候進行了數組的初始化?
Answer
ConcurrentHashMap的構造方法並沒有對數組進行初始化。以傳入初始化大小參數的ConcurrentHashMap爲例。這裏設置了sizeCtl的值。代表下一次擴容的大小。初始化數組的操作延伸到第一次put操作。
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
}
Question–sizeCtl屬性的意義?
Answer
- 負數 代表正在進行初始化或擴容操作
- -1 代表正在初始化
- -N 表示有N-1個線程正在進行擴容操作
- 正數或0 代表hash表還沒有被初始化,表示初始化或下一次進行擴容的大小。始終是ConcurrentHashMap容量的0.75倍,與擴容因子 loadfactor 對應。
Question–ConcurrentHashMap如何取得數組下標,並實現併發場景下安全插入元素?
Answer
元素的hash值通過擾動函數進行再一次運算後,再和 n-1 做 與 運算。取得數組下標。
// n-1 :數組長度減一
i = (n - 1) & hash
實現併發場景下插入元素。
通過cas操作把元素封裝成NODE,插入到tab[i]位置。
- 當有一個線程cas操作成功之後退出循環,由於cas是原子操作。其它線程也能看到**tab[i]**位置被成功插入了元素。從而實現了線程安全。
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
-
若是**tab[i]**位置已經有了元素,則會形成鏈表。此時會有目標節點進行加鎖,並不影響其它節點進行數據的插入。
-
若當前節點是TreeBin類型節點,說明當前節點是紅黑樹根節點,則會在數結構上遍歷元素,更新或插入。
Question–發生hash碰撞時的處理方式,同一鏈表上的元素hash值相同嗎?
Answer
發生hash碰撞時,會在目標數組上構造鏈表。實現數組+鏈表的數據結構。如果鏈表的長度大於8,則會去執行擴容或鏈轉紅黑樹操作。當鏈表的長度大於8,數組長度大於64,鏈轉紅黑樹。若數組長度小於64,則執行擴容操作。
ConcurrentHashMap得到數組下標位置的方式是 經過擾動函數運算得到的 hash 值 和 數組長度減一 做 與 運算。所以,同一鏈表上的元素的hash值不一定相同。
Question–ConcurrentHashMap如何統計元素個數的?
Answer
ConcurrentHashMap統計元素個數,分爲併發場景下計數和非併發場景下的計數。非併發場景下,使用全局變量記錄插入數組的元素個數。併發場景下,使用CounterCell數組來統計元素個數,這裏採用了分而治之的思想,當多個線程同時進行插入數據操作的時候。CounterCell數組中的每一個元素都會被用來存儲元素個數。具體做法是:通過**ThreadLocalRandom.getProbe()**方法會爲每一個線程分配一個唯一的隨機數值,通過計算,每一個線程會去操作數組中的不同位置,分別記錄自己插入成功的元素個數,記錄到指定數組位置。
Question–什麼情況下會觸發鏈轉紅黑樹?
Answer
當某一節點位置的鏈表長度大於8的時候,會觸發鏈轉紅黑樹的操作。但是並不會立即轉化爲紅黑樹,而是會進一步判斷數組的長度是否大於64。如果數組長度大於64,則進行鏈轉紅黑樹的操作。否則進行擴容,把鏈表拆分
成高低鏈,低位鏈位置保持不變,高位鏈位置向後移動 n。n爲數組長度。