併發編程——ConcurrentHashMap#helpTransfer() 分析

前言

ConcurrentHashMap 鬼斧神工,併發添加元素時,若是 map 正在擴容,其餘線程甚至於還會幫助擴容,也就是多線程擴容。就這一點,就能夠寫一篇文章好好講講。今天一塊兒來看看。java

源碼分析

爲何幫助擴容?node

在 putVal 方法中,若是發現線程當前 hash 衝突了,也就是當前 hash 值對應的槽位有值了,且若是這個值是 -1 (MOVED),說明 Map 正在擴容。那麼就幫助 Map 進行擴容。以加快速度。多線程

具體代碼以下:併發

int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
    Node<K,V> f; int n, i, fh;
    if (tab == null || (n = tab.length) == 0)
        tab = initTable();
    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
    } // 若是對應槽位不爲空,且他的 hash 值是 -1,說明正在擴容,那麼就幫助其擴容。以加快速度
    else if ((fh = f.hash) == MOVED)
        tab = helpTransfer(tab, f);
複製代碼

傳入的參數是:成員變量 table 和 對應槽位的 f 變量。源碼分析

怎麼驗證 hash 值是 MOVED 就是正在擴容呢?this

在 Cmap(ConcurrentHashMap 簡稱) 中,定義了一堆常量,其中:spa

static final int MOVED     = -1; // hash for forwarding nodes
複製代碼

hash for forwarding nodes,說明這個爲了移動節點而準備的常量。線程

在 Node 的子類 ForwardingNode 的構造方法中,能夠看到這個變量做爲 hash 值進行了初始化。code

ForwardingNode(Node<K,V>[] tab) {
    super(MOVED, null, null, null);
    this.nextTable = tab;
}
複製代碼

而這個構造方法只在一個地方調用了,即 transfer(擴容) 方法。cdn

點到爲止。

關於擴容後面再開一篇。

好了,如何幫助擴容呢?那要看看 helpTransfer 方法的實現。

/** * Helps transfer if a resize is in progress. */
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
    Node<K,V>[] nextTab; int sc;
    // 若是 table 不是空 且 node 節點是轉移類型,數據檢驗
    // 且 node 節點的 nextTable(新 table) 不是空,一樣也是數據校驗
    // 嘗試幫助擴容
    if (tab != null && (f instanceof ForwardingNode) &&
        (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
    	// 根據 length 獲得一個標識符號
        int rs = resizeStamp(tab.length);
    	// 若是 nextTab 沒有被併發修改 且 tab 也沒有被併發修改
    	// 且 sizeCtl < 0 (說明還在擴容)
        while (nextTab == nextTable && table == tab &&
               (sc = sizeCtl) < 0) {
        	// 若是 sizeCtl 無符號右移 16 不等於 rs ( sc前 16 位若是不等於標識符,則標識符變化了)
        	// 或者 sizeCtl == rs + 1 (擴容結束了,再也不有線程進行擴容)(默認第一個線程設置 sc ==rs 左移 16 位 + 2,當第一個線程結束擴容了,就會將 sc 減一。這個時候,sc 就等於 rs + 1)
        	// 或者 sizeCtl == rs + 65535 (若是達到最大幫助線程的數量,即 65535)
        	// 或者轉移下標正在調整 (擴容結束)
        	// 結束循環,返回 table
            if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                sc == rs + MAX_RESIZERS || transferIndex <= 0)
                break;
            // 若是以上都不是, 將 sizeCtl + 1, (表示增長了一個線程幫助其擴容)
            if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
            	// 進行轉移
                transfer(tab, nextTab);
                // 結束循環
                break;
            }
        }
        return nextTab;
    }
    return table;
}
複製代碼

關於 sizeCtl 變量:

-1 :表明table正在初始化,其餘線程應該交出CPU時間片 -N: 表示正有N-1個線程執行擴容操做(高 16 位是 length 生成的標識符,低 16 位是擴容的線程數) 大於 0: 若是table已經初始化,表明table容量,默認爲table大小的0.75,若是還未初始化,表明須要初始化的大小

代碼步驟:

  1. 判 tab 空,判斷是不是轉移節點。判斷 nextTable 是否更改了。
  2. 更加 length 獲得標識符。
  3. 判斷是否併發修改了,判斷是否還在擴容。
  4. 若是還在擴容,判斷標識符是否變化,判斷擴容是否結束,判斷是否達到最大線程數,判斷擴容轉移下標是否在調整(擴容結束),若是知足任意條件,結束循環。
  5. 若是不知足,併發轉移。

這裏有一個花費了很長時間糾結的地方:

sc == rs + 1
複製代碼

這個判斷能夠在 addCount 方法中找到答案:默認第一個線程設置 sc ==rs 左移 16 位 + 2,當第一個線程結束擴容了,就會將 sc 減一。這個時候,sc 就等於 rs + 1。

若是 sizeCtl == 標識符 + 1 ,說明庫容結束了,沒有必要再擴容了。

總結一下:

當 Cmap put 元素的時候,若是發現這個節點的元素類型是 forward 的話,就幫助正在擴容的線程一塊兒擴容,提升速度。其中, sizeCtl 是關鍵,該變量高 16 位保存 length 生成的標識符,低 16 位保存併發擴容的線程數,經過這連個數字,能夠判斷出,是否結束擴容了。

以下圖:

相關文章
相關標籤/搜索