死磕 java集合之ConcurrentHashMap原始碼分析(二)——擴容全解析

彤哥讀原始碼發表於2019-04-13

?歡迎關注我的公眾號“彤哥讀原始碼”,檢視更多原始碼系列文章, 與彤哥一起暢遊原始碼的海洋。


本章接著上一章,連結直達請點我


初始化桶陣列

第一次放元素時,初始化桶陣列。

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        if ((sc = sizeCtl) < 0)
            // 如果sizeCtl<0說明正在初始化或者擴容,讓出CPU
            Thread.yield(); // lost initialization race; just spin
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            // 如果把sizeCtl原子更新為-1成功,則當前執行緒進入初始化
            // 如果原子更新失敗則說明有其它執行緒先一步進入初始化了,則進入下一次迴圈
            // 如果下一次迴圈時還沒初始化完畢,則sizeCtl<0進入上面if的邏輯讓出CPU
            // 如果下一次迴圈更新完畢了,則table.length!=0,退出迴圈
            try {
                // 再次檢查table是否為空,防止ABA問題
                if ((tab = table) == null || tab.length == 0) {
                    // 如果sc為0則使用預設值16
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    // 新建陣列
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    // 賦值給table桶陣列
                    table = tab = nt;
                    // 設定sc為陣列長度的0.75倍
                    // n - (n >>> 2) = n - n/4 = 0.75n
                    // 可見這裡裝載因子和擴容門檻都是寫死了的
                    // 這也正是沒有threshold和loadFactor屬性的原因
                    sc = n - (n >>> 2);
                }
            } finally {
                // 把sc賦值給sizeCtl,這時儲存的是擴容門檻
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}
複製程式碼

(1)使用CAS鎖控制只有一個執行緒初始化桶陣列;

(2)sizeCtl在初始化後儲存的是擴容門檻;

(3)擴容門檻寫死的是桶陣列大小的0.75倍,桶陣列大小即map的容量,也就是最多儲存多少個元素。

判斷是否需要擴容

每次新增元素後,元素數量加1,並判斷是否達到擴容門檻,達到了則進行擴容或協助擴容。

private final void addCount(long x, int check) {
    CounterCell[] as; long b, s;
    // 這裡使用的思想跟LongAdder類是一模一樣的(後面會講)
    // 把陣列的大小儲存根據不同的執行緒儲存到不同的段上(也是分段鎖的思想)
    // 並且有一個baseCount,優先更新baseCount,如果失敗了再更新不同執行緒對應的段
    // 這樣可以保證儘量小的減少衝突

    // 先嚐試把數量加到baseCount上,如果失敗再加到分段的CounterCell上
    if ((as = counterCells) != null ||
            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        CounterCell a; long v; int m;
        boolean uncontended = true;
        // 如果as為空
        // 或者長度為0
        // 或者當前執行緒所在的段為null
        // 或者在當前執行緒的段上加數量失敗
        if (as == null || (m = as.length - 1) < 0 ||
                (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                !(uncontended =
                        U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
            // 強制增加數量(無論如何數量是一定要加上的,並不是簡單地自旋)
            // 不同執行緒對應不同的段都更新失敗了
            // 說明已經發生衝突了,那麼就對counterCells進行擴容
            // 以減少多個執行緒hash到同一個段的概率
            fullAddCount(x, uncontended);
            return;
        }
        if (check <= 1)
            return;
        // 計算元素個數
        s = sumCount();
    }
    if (check >= 0) {
        Node<K,V>[] tab, nt; int n, sc;
        // 如果元素個數達到了擴容門檻,則進行擴容
        // 注意,正常情況下sizeCtl儲存的是擴容門檻,即容量的0.75倍
        while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                (n = tab.length) < MAXIMUM_CAPACITY) {
            // rs是擴容時的一個郵戳標識
            int rs = resizeStamp(n);
            if (sc < 0) {
                // sc<0說明正在擴容中
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                    // 擴容已經完成了,退出迴圈
                    // 正常應該只會觸發nextTable==null這個條件,其它條件沒看出來何時觸發
                    break;

                // 擴容未完成,則當前執行緒加入遷移元素中
                // 並把擴容執行緒數加1
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt);
            }
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                    (rs << RESIZE_STAMP_SHIFT) + 2))
                // 這裡是觸發擴容的那個執行緒進入的地方
                // sizeCtl的高16位儲存著rs這個擴容郵戳
                // sizeCtl的低16位儲存著擴容執行緒數加1,即(1+nThreads)
                // 所以官方說的擴容時sizeCtl的值為 -(1+nThreads)是錯誤的

                // 進入遷移元素
                transfer(tab, null);
            // 重新計算元素個數
            s = sumCount();
        }
    }
}
複製程式碼

(1)元素個數的儲存方式類似於LongAdder類,儲存在不同的段上,減少不同執行緒同時更新size時的衝突;

(2)計算元素個數時把這些段的值及baseCount相加算出總的元素個數;

(3)正常情況下sizeCtl儲存著擴容門檻,擴容門檻為容量的0.75倍;

(4)擴容時sizeCtl高位儲存擴容郵戳(resizeStamp),低位儲存擴容執行緒數加1(1+nThreads);

(5)其它執行緒新增元素後如果發現存在擴容,也會加入的擴容行列中來;

協助擴容(遷移元素)

執行緒新增元素時發現正在擴容且當前元素所在的桶元素已經遷移完成了,則協助遷移其它桶的元素。

final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
    Node<K,V>[] nextTab; int sc;
    // 如果桶陣列不為空,並且當前桶第一個元素為ForwardingNode型別,並且nextTab不為空
    // 說明當前桶已經遷移完畢了,才去幫忙遷移其它桶的元素
    // 擴容時會把舊桶的第一個元素置為ForwardingNode,並讓其nextTab指向新桶陣列
    if (tab != null && (f instanceof ForwardingNode) &&
            (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
        int rs = resizeStamp(tab.length);
        // sizeCtl<0,說明正在擴容
        while (nextTab == nextTable && table == tab &&
                (sc = sizeCtl) < 0) {
            if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || transferIndex <= 0)
                break;
            // 擴容執行緒數加1
            if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                // 當前執行緒幫忙遷移元素
                transfer(tab, nextTab);
                break;
            }
        }
        return nextTab;
    }
    return table;
}
複製程式碼

當前桶元素遷移完成了才去協助遷移其它桶元素;

遷移元素

擴容時容量變為兩倍,並把部分元素遷移到其它桶中。

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range
    if (nextTab == null) {            // initiating
        // 如果nextTab為空,說明還沒開始遷移
        // 就新建一個新桶陣列
        try {
            // 新桶陣列是原桶的兩倍
            @SuppressWarnings("unchecked")
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            nextTab = nt;
        } catch (Throwable ex) {      // try to cope with OOME
            sizeCtl = Integer.MAX_VALUE;
            return;
        }
        nextTable = nextTab;
        transferIndex = n;
    }
    // 新桶陣列大小
    int nextn = nextTab.length;
    // 新建一個ForwardingNode型別的節點,並把新桶陣列儲存在裡面
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    boolean advance = true;
    boolean finishing = false; // to ensure sweep before committing nextTab
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
        // 整個while迴圈就是在算i的值,過程太複雜,不用太關心
        // i的值會從n-1依次遞減,感興趣的可以打下斷點就知道了
        // 其中n是舊桶陣列的大小,也就是說i從15開始一直減到1這樣去遷移元素
        while (advance) {
            int nextIndex, nextBound;
            if (--i >= bound || finishing)
                advance = false;
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;
            }
            else if (U.compareAndSwapInt
                    (this, TRANSFERINDEX, nextIndex,
                            nextBound = (nextIndex > stride ?
                                    nextIndex - stride : 0))) {
                bound = nextBound;
                i = nextIndex - 1;
                advance = false;
            }
        }
        if (i < 0 || i >= n || i + n >= nextn) {
            // 如果一次遍歷完成了
            // 也就是整個map所有桶中的元素都遷移完成了
            int sc;
            if (finishing) {
                // 如果全部遷移完成了,則替換舊桶陣列
                // 並設定下一次擴容門檻為新桶陣列容量的0.75倍
                nextTable = null;
                table = nextTab;
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                // 當前執行緒擴容完成,把擴容執行緒數-1
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    // 擴容完成兩邊肯定相等
                    return;
                // 把finishing設定為true
                // finishing為true才會走到上面的if條件
                finishing = advance = true;
                // i重新賦值為n
                // 這樣會再重新遍歷一次桶陣列,看看是不是都遷移完成了
                // 也就是第二次遍歷都會走到下面的(fh = f.hash) == MOVED這個條件
                i = n; // recheck before commit
            }
        }
        else if ((f = tabAt(tab, i)) == null)
            // 如果桶中無資料,直接放入ForwardingNode標記該桶已遷移
            advance = casTabAt(tab, i, null, fwd);
        else if ((fh = f.hash) == MOVED)
            // 如果桶中第一個元素的hash值為MOVED
            // 說明它是ForwardingNode節點
            // 也就是該桶已遷移
            advance = true; // already processed
        else {
            // 鎖定該桶並遷移元素
            synchronized (f) {
                // 再次判斷當前桶第一個元素是否有修改
                // 也就是可能其它執行緒先一步遷移了元素
                if (tabAt(tab, i) == f) {
                    // 把一個連結串列分化成兩個連結串列
                    // 規則是桶中各元素的hash與桶大小n進行與操作
                    // 等於0的放到低位連結串列(low)中,不等於0的放到高位連結串列(high)中
                    // 其中低位連結串列遷移到新桶中的位置相對舊桶不變
                    // 高位連結串列遷移到新桶中位置正好是其在舊桶的位置加n
                    // 這也正是為什麼擴容時容量在變成兩倍的原因
                    Node<K,V> ln, hn;
                    if (fh >= 0) {
                        // 第一個元素的hash值大於等於0
                        // 說明該桶中元素是以連結串列形式儲存的
                        // 這裡與HashMap遷移演算法基本類似
                        // 唯一不同的是多了一步尋找lastRun
                        // 這裡的lastRun是提取出連結串列後面不用處理再特殊處理的子連結串列
                        // 比如所有元素的hash值與桶大小n與操作後的值分別為 0 0 4 4 0 0 0
                        // 則最後後面三個0對應的元素肯定還是在同一個桶中
                        // 這時lastRun對應的就是倒數第三個節點
                        // 至於為啥要這樣處理,我也沒太搞明白
                        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;
                        }
                        // 遍歷連結串列,把hash&n為0的放在低位連結串列中
                        // 不為0的放在高位連結串列中
                        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
                        setTabAt(nextTab, i + n, hn);
                        // 標記當前桶已遷移
                        setTabAt(tab, i, fwd);
                        // advance為true,返回上面進行--i操作
                        advance = true;
                    }
                    else if (f instanceof TreeBin) {
                        // 如果第一個元素是樹節點
                        // 也是一樣,分化成兩顆樹
                        // 也是根據hash&n為0放在低位樹中
                        // 不為0放在高位樹中
                        TreeBin<K,V> t = (TreeBin<K,V>)f;
                        TreeNode<K,V> lo = null, loTail = null;
                        TreeNode<K,V> hi = null, hiTail = null;
                        int lc = 0, hc = 0;
                        // 遍歷整顆樹,根據hash&n是否為0分化成兩顆樹
                        for (Node<K,V> e = t.first; e != null; e = e.next) {
                            int h = e.hash;
                            TreeNode<K,V> p = new TreeNode<K,V>
                                    (h, e.key, e.val, null, null);
                            if ((h & n) == 0) {
                                if ((p.prev = loTail) == null)
                                    lo = p;
                                else
                                    loTail.next = p;
                                loTail = p;
                                ++lc;
                            }
                            else {
                                if ((p.prev = hiTail) == null)
                                    hi = p;
                                else
                                    hiTail.next = p;
                                hiTail = p;
                                ++hc;
                            }
                        }
                        // 如果分化的樹中元素個數小於等於6,則退化成連結串列
                        ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                                (hc != 0) ? new TreeBin<K,V>(lo) : t;
                        hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                                (lc != 0) ? new TreeBin<K,V>(hi) : t;
                        // 低位樹的位置不變
                        setTabAt(nextTab, i, ln);
                        // 高位樹的位置是原位置加n
                        setTabAt(nextTab, i + n, hn);
                        // 標記該桶已遷移
                        setTabAt(tab, i, fwd);
                        // advance為true,返回上面進行--i操作
                        advance = true;
                    }
                }
            }
        }
    }
}
複製程式碼

(1)新桶陣列大小是舊桶陣列的兩倍;

(2)遷移元素先從靠後的桶開始;

(3)遷移完成的桶在裡面放置一ForwardingNode型別的元素,標記該桶遷移完成;

(4)遷移時根據hash&n是否等於0把桶中元素分化成兩個連結串列或樹;

(5)低位連結串列(樹)儲存在原來的位置;

(6)高們連結串列(樹)儲存在原來的位置加n的位置;

(7)遷移元素時會鎖住當前桶,也是分段鎖的思想;


未完待續~~


現在文章沒辦法留言了,如果有任何建議或者意見,歡迎大家在公眾號後臺給我留言,謝謝~


歡迎關注我的公眾號“彤哥讀原始碼”,檢視更多原始碼系列文章, 與彤哥一起暢遊原始碼的海洋。

qrcode

相關文章