Java - 深入探究ConcurrentHashMap(二)

基於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爲數組長度。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章