ConcurrentHashMap原始碼閱讀

ifrank98發表於2020-11-26

ConcurrentHashMap原始碼閱讀

稍微有點粗糙的圖片

稍微有點粗糙的圖片

  • ConcurrentHashMap是屬於Java併發包,可以稱之為是執行緒安全的HashMap.(以下有簡稱CHM)

  • 總所周知,HashMap有良好的存取效能,但並不支援併發環境,HashTable支援併發環境,而在存取方法上直接加Synchronized的方式會使效能明顯下降,儘管SynchronizeJDK1.6之後進行了大量的優化,但依舊不是最優選.

  • HashMap陣列+連結串列/紅黑樹的結構基礎上,區別於HashTable中的對整個陣列物件上鎖,ConcurrentHashMap使用為陣列中的每個桶上鎖的機制,不知道還能不能成為分段鎖一個桶就是一個段(JDK1.7採用的是segment,1.8的程式碼中雖然保留了但非常簡短僅為相容)

  • 僅代表個人意見,有錯誤歡迎留言,謝謝!


成員變數

1. transient volatile Node<K,V>[] table;
複製程式碼

實際儲存資料的Node陣列,volatile保證可見性。

2. private transient volatile Node<K,V>[] nextTable;
複製程式碼

下一個使用的陣列,僅在擴容更新的時候不為空,擴容時會慢慢把資料移動到這個陣列.

該陣列作為擴容的過度,類外無法訪問

3. private transient volatile long baseCount;
複製程式碼

在沒有發生爭用時的元素統計

4. private transient volatile int transferIndex;
複製程式碼

擴容索引值,表示已經分配給擴容執行緒的table陣列索引位置,主要用來協調多個執行緒間遷移任務的併發安全.

private transient volatile int sizeCtl;
複製程式碼

重要程度堪比AQSstate,是一個在多執行緒間共享的競態變數,用於維護各種狀態,儲存各類資訊.

  • sizeCtl > 0時可分為兩種情況:

    • 未初始化時,sizeCtl表示初始容量.
    • 初始化後表示擴容的閾值,為當前陣列長度length*0.75
  • sizeCtl = -1: 表示正在初始化或者擴容階段.

  • sizeCtl < -1 : sizeCtl承擔起了擴容時識別符號(高16位)和參與執行緒數目(低16位)的儲存

    • addCounthelpTransfer的方法程式碼中,如果需要幫助擴容,則會CAS替換為sizeCtl+1

    • 在完成當前擴容內容,且沒有再分配的區域時,執行緒會退出擴容,此時會CAS替換為sizeCtl-1

Node
  • 構成連結串列元素的節點類,儲存K/V鍵值對
 static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V val;
        volatile Node<K,V> next;
        Node(int hash, K key, V val, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.val = val;
            this.next = next;
        }
        public final K getKey()       { return key; }
        public final V getValue()     { return val; }
        public final int hashCode()   { return key.hashCode() ^ val.hashCode(); }
        public final String toString(){ return key + "=" + val; }
        public final V setValue(V value) {
            throw new UnsupportedOperationException();
        }
        public final boolean equals(Object o) {
            Object k, v, u; Map.Entry<?,?> e;
            return ((o instanceof Map.Entry) &&
                    (k = (e = (Map.Entry<?,?>)o).getKey()) != null &&
                    (v = e.getValue()) != null &&
                    (k == key || k.equals(key)) &&
                    (v == (u = val) || v.equals(u)));
        }
      	/**
         * Virtualized support for map.get(); overridden in subclasses.
         * 為map.get()提供虛擬化支援;在子類中覆蓋.
         */
        Node<K,V> find(int h, Object k) {
            Node<K,V> e = this;
            if (k != null) {
                do {
                    // 裡面就是普通的連結串列迴圈,直到拿到相應的值
                    K ek;
                    if (e.hash == h &&
                        ((ek = e.key) == k || (ek != null && k.equals(ek))))
                        return e;
                } while ((e = e.next) != null);
            }
            return null;
        }
    }
複製程式碼
  • ConcurrentHashMap,HashMapNode的差別
    • valnext使用volatile關鍵字修飾,確保多執行緒之間的可見性.
    • hashCode方法略有不同,因為ConcurrentHashMap不支援keyvalue為NULL值,所以直接使用key.hashCode() ^ val.hashCode()跳過了為空判斷.
  • find()方法用來在特定時間段幫忙獲取節點後的元素.一般作為桶的頭節點呼叫,用來查詢桶中元素.
ForwardingNode
  • 轉發節點

  • 該類僅僅存活在擴容階段,作為一個標記節點放在桶的首位,並且指向是nextTable(擴容的中間陣列)

  • 從建構函式可知,ForwardingNodehash為-1,其他為空,是個完完全全的輔助類.

static final class ForwardingNode<K,V> extends Node<K,V> {
        final Node<K,V>[] nextTable;
    
    	// 建構函式中預設以MOVED:-1為Hash,其它為空
        ForwardingNode(Node<K,V>[] tab) {
            super(MOVED, null, null, null);
            this.nextTable = tab;
        }
    	// 幫助擴容時的元素查詢
        Node<K,V> find(int h, Object k) {
            // loop to avoid arbitrarily deep recursion on forwarding nodes
            outer: for (Node<K,V>[] tab = nextTable;;) {
                Node<K,V> e; int n;
                if (k == null || tab == null || (n = tab.length) == 0 ||
                    (e = tabAt(tab, (n - 1) & h)) == null)
                    return null;
                for (;;) {
                    int eh; K ek;
                    if ((eh = e.hash) == h &&
                        ((ek = e.key) == k || (ek != null && k.equals(ek))))
                        return e;
                    if (eh < 0) {
                        if (e instanceof ForwardingNode) {
                            tab = ((ForwardingNode<K,V>)e).nextTable;
                            continue outer;
                        }
                        else
                            return e.find(h, k);
                    }
                    if ((e = e.next) == null)
                        return null;
                }
            }
        }
    }
複製程式碼
  • find()方法實在擴容期間幫助get方法獲取桶中元素.

元素新增方法

 	public V put(K key, V value) {
        return putVal(key, value, false);
    }
複製程式碼
  • 按照慣例,暴露在最外面的方法都是直接呼叫的邏輯實現方法.
putVal 存的具體邏輯方法
    /**
	 * 方法引數:
	 * 1. key,value 自然不用說就是k/v的兩個值
	 * 2. onlyIfAbsent 若為true,則僅僅在值為空時覆蓋
	 * 返回值:
	 *  返回舊值,若是新增就為null.
	 */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        // CHM不支援NULL值的鐵證.
        if (key == null || value == null) throw new NullPointerException();
        // 獲得key的Hash,spread可以稱之為擾動函式
        int hash = spread(key.hashCode());
        int binCount = 0;
        // 無限迴圈
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            // 在tab為空時負責初始化Table
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            // 使用`(n-1)&hash`確定了元素的下標位置,獲取對應節點
            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`為`MOVED`,表示該節點是`ForwardingNode`
            // 也就表示陣列正在進行擴容
            else if ((fh = f.hash) == MOVED)
                // 幫助擴容
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                // 上鎖保證原子性,volatile僅能保證可見性
                // f為key獲取到的節點元素,以此為鎖物件
                synchronized (f) {
                    // f在上文就是根據`tabAt(tab,i)`獲取的
                    // 此處是再次獲取驗證有沒有被修改
                    if (tabAt(tab, i) == f) {
                        // 與else.if比較,得知
                        // fh >= 0表示當前節點為連結串列節點,即當前桶結構為連結串列 		  ???
                        if (fh >= 0) {
                            // 連結串列中的元素個數統計
                            binCount = 1;
                            // 迴圈遍歷整個桶
                            // 跳出迴圈的兩種情況:
                            // 1. 找到相同的值,binCount此時表示遍歷的節點個數
                            // 2. 遍歷到末尾,binCount就表示桶中的節點個數
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                // 原始碼中大量運用了表示式的短路特性,來展示判斷的優先順序
                                // 1. 若hash不相等,則直接跳過判斷
                                // 2. hash相等之後,若key的地址相同,則直接進入if
                                // 3. 地址不同時在進入判斷內容是否相等
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    // onlyIfAbsent為true,表示存在時不覆蓋內容
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    // 已經找到確定的元素了,更新不更新都跳出
                                    break;
                                }
                                // 因為e就在同步程式碼塊中,桶已經被上鎖,不可能有別的執行緒改變
                                // 所以不需要重新獲取
                                Node<K,V> pred = e;
                                // 1. 如果e為空,則直接將元素掛接到e後面,跳出迴圈
                                // 2. e不為空,繼續遍歷
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        // 類似HashMap,樹節點獨立操作.
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                // 表示進入了上面的同步表示式,對桶進行修改之後
                if (binCount != 0) {
                    // 如果binCount大於樹的臨界值,就將連結串列轉化為紅黑樹
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    // 如果oldVal部位空,則返回
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        // 新增元素計數,並在binCount大於0時檢查是否需要擴容
        addCount(1L, binCount);
        return null;
    }
複製程式碼
整個的新增流程
  1. 判斷並排除key,value非空,ConcurrentHashMap不支援key或value為空.

  2. 得到擾動後的hash,進入tab陣列的遍歷,若陣列為空則進行初始化

  3. 通過(n - 1) & hash的公式獲取桶的下標 ,若桶為空則直接填充key,value為桶的頭節點

  4. 判斷桶的頭節點hash,若hash == -1表示陣列在擴容並幫助擴容.

  5. 進入synchronize的同步程式碼塊,如果桶的頭節點的hash大於0表示桶的結構為連結串列,接下去就是正常的連結串列遍歷,新增或者覆蓋.

  6. 如果桶的頭節點是TreeBin型別表示桶的結構為紅黑樹,按紅黑樹的操作進行遍歷.

  7. 退出同步程式碼塊,判斷在遍歷期間統計的binCount是否需要轉化為紅黑樹結構.

  8. 判斷oldVal是否為空,這步也挺關鍵的,如果不為空表示時覆蓋操作,直接return就好,不需要檢查擴容.

  9. 如果oldVal不為空呼叫addCount方法新增元素個數,並檢測是否需要擴容.

元素獲取方法

   public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
       	// 獲取hash,並進過擾動
        int h = spread(key.hashCode());
       	// 判斷以進入獲取方法
       	// 1. 陣列不為空 & 陣列長度大於0
        // 2. 獲取的桶不為空
        if ((tab = table) != null && (n = tab.length) > 0 &&
            // 獲取桶下標的公式都是通用的 `(n -1) & h`
            (e = tabAt(tab, (n - 1) & h)) != null)
        {// 對於桶中頭節點的hash,對比成功就不需要遍歷整個列表了
            if ((eh = e.hash) == h) {
                // 返回匹配的元素value
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            // 元素hash < 0的情況有以下三種:
            // 1. 陣列正在擴容,Node的實際型別是ForwardingNode
            // 2. 節點為樹的root節點,TreeNode
            // 3. 暫時保留的Hash, Node
            // 不同的Node都會呼叫各自的find()方法
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            // 如果頭節點不是所需節點,且Map此時並未擴容
        	// 直接遍歷桶中元素查詢
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }
複製程式碼
完整的獲取流程如下:
  1. 經過擾動函式獲取key的hash,在獲取之前會先判斷tab是否為空以及長度
  2. 通過(n -1)& hash獲取的桶下表獲取桶.
  3. 判斷key的hash和桶的頭節點是否相等,相等則直接返回.
  4. 若獲得的桶頭節點的hash < 0,表示處於以下三種狀態,則是通過呼叫各自實際節點型別的find方法獲取元素.
    1. 陣列正在擴容,Node的實際型別是ForwardingNode
    2. 節點為樹的root節點,節點型別為TreeNode
    3. 暫時保留的Hash, Node
  5. 如果hash不相等,且頭節點hash正常,之後就是普通的連結串列遍歷查詢操作.

擴容機制

  • 不得不說,擴容部分的程式碼絕對是超一流的大師手筆!!!
addCount 擴容的監測
  • addCount的作用:
    1. 增加ConcurrentHashMap的元素計數
    2. 前驅檢測是否需要擴容,
/**
 *   引數: 
 * 	 x -> 具體增加的元素個數
 *   check -> 如果check<0不檢查時都需要擴容,
 */
private final void addCount(long x, int check) {
        CounterCell[] as; long b, s;
     	// 1. counterCells不為空
      	// 2. CAS修改baseCount屬性成功
        if ((as = counterCells) != null ||
            // CAS增加baseCOunt
            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            CounterCell a; long v; int m;
            // 執行緒爭用的狀態標記
            boolean uncontended = true;
            // 1. 計數cell為null,或長度小於1
            // 2. 隨機去一個陣列位置為為空
            // 3. CAS替換CounterCell的value失敗
            if (as == null || (m = as.length - 1) < 0 ||
                (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
            // CAS增加CounterCell的value值失敗會呼叫fullAddCount方法
                !(uncontended =
                  U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
                fullAddCount(x, uncontended);
                return;
            }
            if (check <= 1)
                return;
            s = sumCount();
        }
    	// 根據`check >= 0`判斷是否需要檢查擴容
        if (check >= 0) {
            Node<K,V>[] tab, nt; int n, sc;
            // 1. 如果元素總數大於sizeCtl,表示達到了擴容閾值
            // 2. tab陣列不能為空,已經初始化
            // 3. table.length小於最大容,有擴容空間
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (n = tab.length) < MAXIMUM_CAPACITY) {
                // 根據陣列長度獲取一個擴容標誌
                int rs = resizeStamp(n);
                if (sc < 0) {
                    // 如果sc的低16位不等於rs,表示識別符號已經改變.				// 待補充
                    // 如果nextTable為空,表示擴容已經結束
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                    // CAS替換sc值為sc+1,成功則開始擴容
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        //	呼叫transfer開始擴容,此時nextTable已經指定
                        transfer(tab, nt);
                }
                // `sc > 0`表示陣列此時並不在擴容階段,更新sizeCtl並開始擴容
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    // 呼叫transfer,nextTable待生成
                    transfer(tab, null);
                s = sumCount();
            }
        }
    }
複製程式碼
helpTransfer 幫助擴容
 /**
  * 引數:
  * tab -> 擴容的陣列,一般為table
  * f -> 執行緒持有的鎖對應的桶的頭節點
  * 呼叫地方:
  * 1. `putVal`檢測到頭節點Hash為MOVED
  */
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
        Node<K,V>[] nextTab; int sc;
        // 1.引數陣列不能為空 
		// 2.引數f必須為ForwardingNode型別
        // 3.f.nextTab不能為空
        if (tab != null && (f instanceof ForwardingNode) &&
            (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
            // resizeStamp一頓位操作打的我頭昏腦漲
            // 獲取擴容的標識
            int rs = resizeStamp(tab.length);
            // Map仍處在擴容狀態的判斷
            // 1. 判斷節點f的nextTable是否和成員變數的nextTable相同
            // 2. 判斷傳入的tab和成員變數的table是否相同
            // 3. sizeCtl是否小於0
            while (nextTab == nextTable && table == tab &&
                   (sc = sizeCtl) < 0) {
                // 兩種不同的情況判斷                
                // 一. 不需要幫助擴容的情況
                // 1. sc的高16位不等於rs
                // 2. sc等於rs+1
                // 3. sc等於rs+MAX_RESIZERS
                // 4. transferIndex <= 0, 這個好理解因為擴容時會分配並減去transferIndex,
                // 小於0時表示陣列的區域已分配完畢
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || transferIndex <= 0)
                    break;
                // 二. CAS `sc+1`並呼叫transfer幫助擴容.
                // 執行緒在幫助擴容時會對sizeCtl+1,完成時-1,表示標記
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                    transfer(tab, nextTab);
                    break;
                }
            }
            return nextTab;
        }
        return table;
    }
複製程式碼
transfer 擴容的核心方法,負責遷移桶中元素
  private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
      	// stride為此次需要遷移的桶的數目
      	// NCPU為當前主機CPU數目
      	// MIN_TRANSFER_STRIDE為每個執行緒最小處理的組數目
      	// 1. 在多核中stride為當前容量的1/8對CPU數目取整,例如容量為16時,CPU為2時結果是1
      	// 2. 在單核中stride為n就為當前陣列容量
 		// !!! stride最小為16,被限定死.
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // subdivide range
      	// nextTab是擴容的過渡物件,所以必須要先初始化
        if (nextTab == null) {            // initiating
            try {
                @SuppressWarnings("unchecked")
                // !!! 重點就在這 擴容後的大小為當前的兩倍 --> n << 1
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
                nextTab = nt;
            } catch (Throwable ex) {      // try to cope with OOME
            	// 擴容失敗,直接填充int的最大值
                sizeCtl = Integer.MAX_VALUE;
                // 直接退出
                return;	
           }
            // 更新成員變數
            nextTable = nextTab;
            // transferIndex為陣列長度
            transferIndex = n;
        }
      	// 記錄過渡陣列的長度
        int nextn = nextTab.length;
    	// 此處新建了一個ForwardingNode用於後續佔位
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        /**
          * 以上為資料準備部分,初始化過渡陣列,記錄長度,建立填充節點等操作
          * 以下時真正擴容的主要邏輯
          */
 		// 該變數控制遷移的進行,     
        boolean advance = true;
        boolean finishing = false; 			// 兩個變數作用未知 finishing可能是此次擴容標記
  // 擴容的for迴圈裡面可以分為兩部分
 // 一. while迴圈裡面確定需要遷移的桶的區域,以及本次需要遷移的桶的下標
      	// 這個i就是需要遷移的桶的下標
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
          	// 該while程式碼塊根據if的順序功能分別是
            // --i: 負責遷移區域的向前推薦,i為桶下標
            // nextIndex: 在沒有獲取負責區域時,檢查是否還需要擴容
            // CAS: 負責獲取此次for迴圈的區域,每次都為stride個桶
            while (advance) {
                int nextIndex, nextBound;
                // 這個`--i`每次都會進行,每次都會向前推進一個位置
                if (--i >= bound || finishing)
                    advance = false;
                // 因此如果當transferIndex<=0時,表示擴容的區域分配完
                else if ((nextIndex = transferIndex) <= 0) {
            		i = -1;
                    advance = false;
                // CAS替換transferIndex的值,新值為舊值減去分到的stride
                // stride就表示此次的遷移區域,nextIndex就代表了下次起點
                // 從這裡可以看出擴容是從陣列末尾開始向前推進的
                }else if (U.compareAndSwapInt
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {
                    // bount為此次擴容的推進終點,下次起點
                    bound = nextBound;
                    // i此次擴容開始的桶下表
                    i = nextIndex - 1;
                    advance = false;
                }
            }
 // 二. 擴容的邏輯程式碼
        // 1. 此if判定擴容的結果,中間是三種異常值
              // 1). i < 0的情況時上面第二個if跳出的執行緒
          	  // 2). i > 舊陣列的長度
           	  // 3). i+n大於新陣列的長度
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                // 此階段擴容結束後的操作
                // 1. 將nextTable置空,
                // 2. 將中間過渡的陣列賦值給table
                // 3. sizeCtl變為1.5倍(2n-0.5n)
                if (finishing) {
                    nextTable = null;
                    table = nextTab;
                    // 分別使用有符號左移,無符號右移
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                // CAS替換`sizeCtl-1`,表示本執行緒的擴容任務已經完成
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    //	表示式成立表示還有別的執行緒在執行擴容,直接退出
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    // 表示式成立,表示已經全部擴容完成.
                    finishing = advance = true;
                    // 提交前重新檢查
                    i = n; 
                }
            }
       // 2. 擴容時發現負責的區域有空的桶直接使用ForwardingNode填充
            // ForwardingNode持有nextTable的引用
            else if ((f = tabAt(tab, i)) == null)
                // CAS替換
                advance = casTabAt(tab, i, null, fwd);
       // 3. 表示處理完畢
            else if ((fh = f.hash) == MOVED)
                advance = true; // already processed
      // 4. 遷移桶的操作
            else {
                // sync保證原子性和可見性
                synchronized (f) {                
                    // 獲取陣列中的第i個桶的頭節點
                    // 進入synchronized之後重新判斷,保證資料的正確性沒有在中間被修改
                    if (tabAt(tab, i) == f) {
                        // 此處擴容和HashMap有點像,分為了lowNode和highNode兩個頭結點
                        Node<K,V> ln, hn;
                        if (fh >= 0) {
                            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);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                           	// true的話會重新
                            advance = true;
                        }
                        // 樹的桶遷移操作
                        else if (f instanceof TreeBin) {
                            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;
                            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;
                                }
                            }
                            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);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                    }
                }
            }
        }
    }

複製程式碼
擴容觸發執行緒邏輯
  1. addCount方法中間檢查元素個數是否達到擴容閾值(0.75 * table.length),超過則觸發擴容,呼叫方法transfer.

    • 注意此時sizeCtl會被CAS替換為(resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2
  2. 接下來就是teansfer的程式碼:

    1. 根據CPU和當前容量算出每次擴容該分配的區域大小,最小為16,表示為stride.

    2. 若過渡陣列nextTab未初始化,則先初始化陣列.並使用transferIndex記錄下舊陣列長度,作為擴容長度.

    3. 以上擴容需要的資料準備完全開始具體的擴容操作:

    4. 在一個while迴圈中獲取本次擴容包含的桶的範圍,即[transferIndex,transferIndex-stride]的範圍,i表示當前擴容的桶的下標.

    5. 三個判斷,四段程式碼分別完成不同情況下的操作

      1. i數值異常,< 0 || >= n || + n > nextn,表示擴容已完成,且在while迴圈中沒有分配的擴容任務.

        • 如果此時finishing引數為true表示整體擴容完成,且完成結束前的檢查.

        • 如果finishingfalse,則**CAS替換sizeCtl為sizeCtl-1**,表示一個執行緒完成擴容任務並需要退出.

          替換成功之後還會檢查sc是否等於addCount進來時的值,不相等就直接return,表示還有執行緒未完成擴容任務.

      2. i對應的桶為空,直接使用ForwardingNode填充頭節點,表示此處正在擴容.並設advancetrue

      3. 如果檢查到節點hashMoved表示當前節點為ForwardingNode,advancetrue.

      4. 排除了上面三種情況,就是對應的桶的遷移工作,和HashMap有點像.結束後設定advancetrue

    6. 之後會再回到第4步.

擴容從屬執行緒邏輯
  1. putVal等元素操作方法中,發現獲取的桶頭節點為ForwdingNode就表示ConcurrentHashMap當前正在擴容,會馬上呼叫helpTransfer幫助擴容.
  2. helpTransfer中會有各種正確性判斷,只有在以下三個條件都都滿足時才會幫助擴容.
    1. tab是否不空
    2. 頭節點是否為ForwardingNode
    3. 過渡陣列nextTable是否初始化.
  3. while迴圈中有以下兩種判斷
    1. 判斷擴容過程是否需要幫助,有以下五種情況不需要幫助
      1. sc >> 16 != rs - 識別符號已經改變.
      2. sc == rs+1 - 觸發擴容的執行緒已退出,擴容已經完成
      3. sc == rs+MAX_RESIZER - 參與擴容的執行緒達最大值
      4. transferIndex <= 0 - 擴容區域已經分配完
    2. 排除以上不需要幫助的情況,就會呼叫transfer幫助擴容.
擴容過程中sizeCtl的變化
  1. addCount -> sizeCtl = (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2

    • 此處有個我很久才想通的點: 為什麼在helpTransfer中會有判斷sizeCtl高16位的操作,
    • 在此處賦值的時候就相當於將resizeStamp(n)的值推高16位,賦值給sizeCtl,而低16位則儲存了這個2.也就是說在擴容的時候sizeCtl的高16為儲存了識別符號,而低16位儲存了參與執行緒數目.
    • 真他孃的是個天才.

    ConcurrentHashMap原始碼閱讀

  2. 有執行緒參與擴容 -> sizeCtl = sizeCtl - 1

  3. 執行緒退出擴容 -> sizeCtl = sizeCtl + 1

  4. 擴容完成 -> sizeCtl = nextTab.length * 0.75

初始化方法

  • HashMap一樣,ConcurrentHashMap並不是在建構函式中就直接初始化底層的陣列,而是在put等存方法中,判斷是否需要擴容.
initTable 陣列初始化函式
private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        // `sizeCtl`表示有別的陣列正在初始化,讓出CPU時間
        if ((sc = sizeCtl) < 0)
            Thread.yield(); // lost initialization race; just spin
        // CAS操作,以-1置換`sizeCtl`的值
        // 可以看出 `sizeCtl==-1`時,表示陣列正在某個執行緒初始化
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                // 置換之後需要重新檢測陣列是否未初始化
                if ((tab = table) == null || tab.length == 0) {
                    // sc就是置換之前的sizeCtl.
                    // 此時sizeCtl作為初始容量.
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    // 初始化結束之後sc變為0.75n,是擴容閾值
                    sc = n - (n >>> 2);
                }
            } finally {
                // 為避免異常退出導致sizeCtl永久為-1,此處強制賦值.
                sizeCtl = sc;
            }
            break;
        }
    }
    // 返回了新建的陣列地址
    return tab;
}
複製程式碼
  • initTable方法時在putVal而非建構函式,也算是CHM中的一種懶載入機制.
  • 初始化的流程:
    1. 檢查是否有別的執行緒正在初始化,有就讓出時間片,沒有則進行下一步.
    2. 初始化之前,先將sizeCtl通過CAS置換為-1,表示正在初始化
    3. sizeCtl之前的值為初始容量,sizeCtl<=0時使用預設容量16
    4. 初始化結束,sizeCtl賦值為0.75*陣列容量(sizeCtl貫穿全篇,真的很重要)

通用工具方法

1. resizeStamp 獲取擴容時的一個標記
    static final int resizeStamp(int n) {
        return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
    }
複製程式碼
  • Integer.numberOfLeadingZeros(n) 返回的是n的32位二進位制形式前面的0的個數,例如值位16的int(32位)型別二進位制表示為000000...0010000,1前面的就有27個0,返回就是27.
  • |操作現在此處可以簡單理解為加法。
  • 整合起來作用就是:獲取n的有效位之前的0的個數加上1的15次方.
  • 暫時不清楚為什麼要獲取一個標Stamp
2. spread 擾動函式
static final int spread(int h) {
    return (h ^ (h >>> 16)) & HASH_BITS;
}
複製程式碼
  • 擾動函式,和HashMap中的hash()方法功能類似.
  • CHM中的擾動函式除了將高16位於低16位異或之外又與上HASH_BITS,可以有效降低雜湊衝突的概率,使元素分散更加均勻.

Node陣列的元素訪問方法

1. tabAt 以Volatile方式獲取陣列元素
    @SuppressWarnings("unchecked")
	// tab: 陣列   i : 下標
    static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
        return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
    }
複製程式碼
2. casTabAt 以CAS形式替換陣列元素
// tab: 原始陣列  i:下標 c:對比元素 v:替換元素   
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                        Node<K,V> c, Node<K,V> v) {
        return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
    }
複製程式碼
3. setTabAt 以volatile方式更新陣列元素
// tab:原始陣列 i:下標 v:替換元素
	static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
        U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
    }
複製程式碼

Unsafe 靜態塊

  • Unsafe是一塊Java開發人員都很少接觸的區域,但這裡還是簡單瞭解一下
    private static final sun.misc.Unsafe U;
	// sizeCtl屬性的偏移地址
    private static final long SIZECTL;
	// transferIndex屬性的偏移地址
    private static final long TRANSFERINDEX;
	// baseCount的偏移地址
    private static final long BASECOUNT;
	// cellsBusy的偏移地址
    private static final long CELLSBUSY;
	// CounterCell類中value的偏移地址
    private static final long CELLVALUE;
	// Node陣列第一個元素的偏移地址
    private static final long ABASE;
	// Node陣列中元素的增量地址,與ABASE配合使用能訪問到陣列的各元素
    private static final int ASHIFT;

    static {
        try {
            U = sun.misc.Unsafe.getUnsafe();
            Class<?> k = ConcurrentHashMap.class;
            // 先通過反射獲取到對應的屬性值,再通過Unsafe類獲取屬性的偏移地址
            SIZECTL = U.objectFieldOffset
                (k.getDeclaredField("sizeCtl"));
            TRANSFERINDEX = U.objectFieldOffset
                (k.getDeclaredField("transferIndex"));
            BASECOUNT = U.objectFieldOffset
                (k.getDeclaredField("baseCount"));
            CELLSBUSY = U.objectFieldOffset
                (k.getDeclaredField("cellsBusy"));
            Class<?> ck = CounterCell.class;
            CELLVALUE = U.objectFieldOffset
                (ck.getDeclaredField("value"));
            Class<?> ak = Node[].class;
            // 獲取陣列中第一個元素的偏移地址
            ABASE = U.arrayBaseOffset(ak);
            // 獲取陣列的增量地址
            int scale = U.arrayIndexScale(ak);
            if ((scale & (scale - 1)) != 0)
                throw new Error("data type scale not a power of two");
            ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);
        } catch (Exception e) {
            throw new Error(e);
        }
    }
複製程式碼

相關文章