Java集合之ConcurrentHashMap.addCount解析

  ConcurrentHashMap 精華代碼很多,前面分析了 helpTransfer 和 transfer 和 putVal 方法,今天來分析一下 addCount 方法,該方法會在 putVal 方法中調用。

  一起來看看 addCount 是如何操作的。

  源碼分析

/**
 * 變更容器大小
 * 
 * @param x     添加的元素個數
 * @param check if < 0, 不檢查調整, if <= 1 只有在未競爭時才檢查
 */
private final void addCount(long x, int check) {
    // 計數盒子
    CounterCell[] as; 
    long b, s;

    // 當計數盒子不爲null 或 通過cas直接更新baseCount失敗
    if ((as = counterCells) != null ||
        !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        CounterCell a; long v; int m;

        // 無競爭標記
        boolean uncontended = true;

        // 當計數盒子爲null 或 計數盒子長度爲0 
        // 或 隨機取計數盒子中的1個元素如果爲null 或 通過CAS直接更新該隨機元素失敗
        if (as == null || (m = as.length - 1) < 0 ||
            (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
            !(uncontended =
              U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
            // 存在競爭更新
            fullAddCount(x, uncontended);
            return;
        }

        // 是否需要進行擴容檢查
        if (check <= 1)
            return;

        // 獲取當前容器元素個數
        s = sumCount();
    }

    if (check >= 0) {
        Node<K,V>[] tab, nt; int n, sc;

        // sizeCtl 閾值(僅在數組未初始化時,等於數據容量)
        // 如果map.size() 大於 sizeCtl(達到擴容閾值需要擴容) 
        // 且table 不是空;且 table 的長度小於 1 << 30。(可以擴容)
        while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
               (n = tab.length) < MAXIMUM_CAPACITY) {

            // 根據 length 得到一個標識
            int rs = resizeStamp(n);

            // 是否正在擴容
            if (sc < 0) {
                // 如果 sc 的低 16 位不等於 標識符(校驗異常 sizeCtl 變化了)
                // 如果 sc == 標識符 + 1 (擴容結束了,不再有線程進行擴容)
                //(默認第一個線程設置 sc == rs 左移 16 位 + 2,當第一個線程結束擴容了,就會將 sc 減 1。
                //     這個時候,sc 就等於 rs + 1)
                // 如果 sc == 標識符 + 65535(幫助線程數已經達到最大)
                // 如果 nextTable == null(結束擴容了)
                // 如果 transferIndex <= 0 (轉移狀態變化了)
                // 結束循環
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                    transferIndex <= 0)
                    break;

                // 如果可以幫助擴容,那麼將 sc 加 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 爲負數後,開始擴容。
                transfer(tab, null);

            s = sumCount();
        }
    }
}

  CounterCell數組的作用是什麼呢?ConcurrentHashMap是採用CounterCell數組來記錄元素個數的,像一般的集合記錄集合大小,直接定義一個size的成員變量即可,當出現改變的時候只要更新這個變量就行。爲什麼ConcurrentHashMap要用這種形式來處理呢? 問題還是處在併發上,ConcurrentHashMap是併發集合,如果用一個成員變量來統計元素個數的話,爲了保證併發情況下共享變量的的安全,勢必會需要通過加鎖或者自旋來實現,如果競爭比較激烈的情況下,size的設置上會出現比較大的衝突反而影響了性能,所以在ConcurrentHashMap採用了分片的方法來記錄大小,具體什麼意思,我們來分析下

public class ConcurrentHashMap<K,V> extends Abstract{
 
    ...
 
    // 標識當前cell數組是否在初始化或擴容中的CAS標誌位
    private transient volatile int cellsBusy;
 
    // counterCells數組,總數值的分值分別存在每個cell中
    private transient volatile CounterCell[] counterCells;
 
    @sun.misc.Contended 
    static final class CounterCell {
     
        volatile long value;
         
        CounterCell(long x) { value = x; }
    }
 
    //看到這段代碼就能夠明白了,CounterCell數組的每個元素,都存儲一個元素個數,而實際我們調用size方法就是通過這個循環累加來得到的
     
    //又是一個設計精華,大家可以借鑑; 有了這個前提,再會過去看addCount這個方法,就容易理解一些了
    final long sumCount() {
        CounterCell[] as = counterCells; CounterCell a;
        long sum = baseCount;
        if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    sum += a.value;
            }
        }
        return sum;
    }
}

  總結一下

  總結下來看,addCount 方法做了 2 件事情:

  1. 對 table 的長度加一。無論是通過修改 baseCount,還是通過使用 CounterCell。當 CounterCell 被初始化了,就優先使用他,不再使用 baseCount。

  2. 檢查是否需要擴容,或者是否正在擴容。如果需要擴容,就調用擴容方法,如果正在擴容,就幫助其擴容。

  有幾個要點注意:

  1. 第一次調用擴容方法前,sizeCtl 的低 16 位是加 2 的,不是加一。所以 sc == rs + 1 的判斷是表示是否完成任務了。因爲完成擴容後,sizeCtl == rs + 1。

  2. 擴容線程最大數量是 65535,是由於低 16 位的位數限制。

  3. 這裏也是可以幫助擴容的,類似 helpTransfer 方法。

  參考資料

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