【死磕 Java 集合】— ConcurrentHashMap源碼分析(二)

 

【死磕 Java 集合】— ConcurrentHashMap源碼分析(二)

 

初始化桶數組

第一次放元素時,初始化桶數組。

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)遷移元素時會鎖住當前桶,也是分段鎖的思想;

 

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