多執行緒高併發程式設計(10) -- ConcurrentHashMap原始碼分析

碼猿手發表於2020-06-14

  一.背景

  前文講了HashMap的原始碼分析,從中可以看到下面的問題:

  • HashMap的put/remove方法不是執行緒安全的,如果在多執行緒併發環境下,使用synchronized進行加鎖,會導致效率低下;
  • 在遍歷迭代獲取時進行修改(put/remove)操作,會導致發生併發修改異常(ConcurrentModificationException);
  • 在JDK1.7之前,對HashMap進行put新增操作,會導致連結串列反轉,造成連結串列迴路,從而發生get死迴圈,(當然這個問題在JDK1.8被改進了按照原連結串列順序進行重排移動);
  • 如果多個執行緒同時檢測到元素個數超過 陣列大小 * loadFactor,這樣就會發生多個執行緒同時對陣列進行擴容,都在重新計算元素位置以及複製資料,但是最終只有一個執行緒擴容後的陣列會賦給 table,也就是說其他執行緒的都會丟失,並且各自執行緒 put 的資料也丟失;

  基於上述問題,都可以使用ConcurrentHashMap進行解決,ConcurrentHashMap使用分段鎖技術解決了併發訪問效率,在遍歷迭代獲取時進行修改操作也不會發生併發修改異常等等問題。

  二.原始碼解析

  1. 構造方法:

    //最大容量大小
        private static final int MAXIMUM_CAPACITY = 1 << 30;
        //預設容量大小
        private static final int DEFAULT_CAPACITY = 16;
        /**
         *控制識別符號,用來控制table的初始化和擴容的操作,不同的值有不同的含義
         *  多執行緒之間,以volatile方式讀取sizeCtl屬性,來判斷ConcurrentHashMap當前所處的狀態。
         *  通過cas設定sizeCtl屬性,告知其他執行緒ConcurrentHashMap的狀態變更
         *未初始化:
         *  sizeCtl=0:表示沒有指定初始容量。
         *  sizeCtl>0:表示初始容量。
         *初始化中:
         *  sizeCtl=-1,標記作用,告知其他執行緒,正在初始化
         *正常狀態:
         *  sizeCtl=0.75n ,擴容閾值
         *擴容中:
         *  sizeCtl < 0 : 表示有其他執行緒正在執行擴容
         *  sizeCtl = (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2 :表示此時只有一個執行緒在執行擴容
         */
        private transient volatile int sizeCtl;
        //併發級別
        private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
        //建立一個新的空map,預設大小是16
        public ConcurrentHashMap() {
        }
        public ConcurrentHashMap(int initialCapacity) {
            if (initialCapacity < 0)
                throw new IllegalArgumentException();
            //調整table的大小,tableSizeFor的實現檢視前文HashMap原始碼分析的構造方法模組
            int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                       MAXIMUM_CAPACITY :
                       tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
            this.sizeCtl = cap;
        }
        public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
            this.sizeCtl = DEFAULT_CAPACITY;
            putAll(m);
        }
        public ConcurrentHashMap(int initialCapacity, float loadFactor) {
            this(initialCapacity, loadFactor, 1);
        }
        /**
         * concurrencyLevel:併發度,預估同時運算元據的執行緒數量
         * 表示能夠同時更新ConccurentHashMap且不產生鎖競爭的最大執行緒數。
         * 預設值為16,(即允許16個執行緒併發可能不會產生競爭)。
         */
        public ConcurrentHashMap(int initialCapacity,
                                 float loadFactor, int concurrencyLevel) {
            if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
                throw new IllegalArgumentException();
            //至少使用同樣多的桶容納同樣多的更新執行緒來操作元素
            if (initialCapacity < concurrencyLevel)   // Use at least as many bins
                initialCapacity = concurrencyLevel;   // as estimated threads
            long size = (long)(1.0 + (long)initialCapacity / loadFactor);
            int cap = (size >= (long)MAXIMUM_CAPACITY) ?
                MAXIMUM_CAPACITY : tableSizeFor((int)size);
            this.sizeCtl = cap;
        }
  2. put:

    public V put(K key, V value) {
            return putVal(key, value, false);
        }
        static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash普通節點雜湊的可用位
        //把位數控制在int最大整數之內,h ^ (h >>> 16)的含義檢視前文的put原始碼解析
        static final int spread(int h) {
            return (h ^ (h >>> 16)) & HASH_BITS;
        }
        final V putVal(K key, V value, boolean onlyIfAbsent) {
            //key和value為空丟擲異常
            if (key == null || value == null) throw new NullPointerException();
            //得到hash值
            int hash = spread(key.hashCode());
            int binCount = 0;
            //自旋對table進行遍歷
            for (Node<K,V>[] tab = table;;) {
                Node<K,V> f; int n, i, fh;
                //初始化table
                if (tab == null || (n = tab.length) == 0)
                    tab = initTable();
                //如果hash計算出的槽位元素為null,CAS將元素填充進當前槽位並結束遍歷
                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);
                else {
                    V oldVal = null;
                    synchronized (f) {// 同步 f 節點,防止增加連結串列的時候導致連結串列成環
                        if (tabAt(tab, i) == f) {// 如果對應的下標位置的節點沒有改變
                            if (fh >= 0) {//f節點的hash值大於0
                                binCount = 1;//連結串列初始長度
                                // 死迴圈,直到將值新增到連結串列尾部,並計算連結串列的長度
                                for (Node<K,V> e = f;; ++binCount) {
                                    K ek;
                                    //hash和key相同,值進行覆蓋
                                    if (e.hash == hash &&
                                        ((ek = e.key) == key ||
                                         (ek != null && key.equals(ek)))) {
                                        oldVal = e.val;
                                        if (!onlyIfAbsent)
                                            e.val = value;
                                        break;
                                    }
                                    Node<K,V> pred = e;
                                    //hash和key不同,新增到連結串列後面
                                    if ((e = e.next) == null) {
                                        pred.next = new Node<K,V>(hash, key,
                                                                  value, null);
                                        break;
                                    }
                                }
                            }
                            //是樹節點,新增到樹中
                            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) {
                        //連結串列長度大於等於8時,將連結串列轉換成紅黑樹樹
                        if (binCount >= TREEIFY_THRESHOLD)
                            treeifyBin(tab, i);
                        if (oldVal != null)
                            return oldVal;
                        break;
                    }
                }
            }
            // 判斷是否需要擴容
            addCount(1L, binCount);
            return null;
        }
    1. initTable:初始化

      private final Node<K,V>[] initTable() {
              Node<K,V>[] tab; int sc;
              while ((tab = table) == null || tab.length == 0) {
                  //如果一個執行緒發現sizeCtl<0,意味著另外的執行緒執行CAS操作成功,當前執行緒只需要讓出cpu時間片,即保證只有一個執行緒初始化
                  //由於sizeCtl是volatile的,保證了順序性和可見性
                  if ((sc = sizeCtl) < 0)
                      Thread.yield(); // lost initialization race; just spin
                  else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {//cas操作判斷並置為-1
                      try {
                          if ((tab = table) == null || tab.length == 0) {
                              int n = (sc > 0) ? sc : DEFAULT_CAPACITY;//若沒有引數則預設容量為16
                              @SuppressWarnings("unchecked")
                              Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];//建立陣列
                              table = tab = nt;//陣列賦值給當前ConcurrentHashMap
                              //計算下一次元素到達擴容的閥值,如果n為16的話,那麼這裡 sc = 12,其實就是 0.75 * n
                              sc = n - (n >>> 2);
                          }
                      } finally {
                          sizeCtl = sc;
                      }
                      break;
                  }
              }
              return tab;
          }
    2. tabAt:尋找指定陣列在記憶體中i位置的資料

      static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
              /**getObjectVolatile:獲取obj物件中offset偏移地址對應的object型field的值,支援volatile load語義。
               * 陣列的定址計算方式:a[i]_address = base_address + i * data_type_size
               * base_address:起始地址;i:索引;data_type_size:資料型別長度大小
               */
              return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
          }
    3. helpTransfer:幫助擴容

      private static int RESIZE_STAMP_BITS = 16;
          /**
           * numberOfLeadingZeros()的具體演算法邏輯請參考:https://www.jianshu.com/p/2c1be41f6e59
           * numberOfLeadingZeros(n)返回的是n的二進位制標識的從高位開始到第一個非0的數字的之間0的個數,比如numberOfLeadingZeros(8)返回的就是28 ,因為0000 0000 0000 0000 0000 0000 0000 1000在1前面有28個0
           * RESIZE_STAMP_BITS 的值是16,1 << (RESIZE_STAMP_BITS - 1)就是將1左移位15位,0000 0000 0000 0000 1000 0000 0000 0000
           * 然後將兩個數字再按位或,將相當於 將移位後的 兩個數相加。
           * 比如:
           * 8的二進位制表示是: 0000 0000 0000 0000 0000 0000 0000 1000 = 8
           * 7的二進位制表示是: 0000 0000 0000 0000 0000 0000 0000 0111 = 7
           * 按位或的結果是:  0000 0000 0000 0000 0000 0000 0000 1111 = 15
           * 相當於 8 + 7 =15
           * 為什麼會出現這種效果呢?因為8是2的整數次冪,也就是說8的二進位制表示只會在某個高位上是1,其餘地位都是0,所以在按位或的時候,低位表示的全是7的位值,所以出現了這種效果。
           */
          static final int resizeStamp(int n) {
              return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
          }
          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 減1。這個時候,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;//沒有擴容,返回原陣列
          }
    4.  transfer:擴容和資料遷移,採用多執行緒擴容,整個擴容過程,通過cas設定sizeCtl、transferIndex等變數協調多個執行緒進行併發擴容;

      1.  transferIndex屬性:
        //擴容索引,表示已經分配給擴容執行緒的table陣列索引位置。主要用來協調多個執行緒,併發安全地獲取遷移任務(hash桶)。
        private transient volatile int transferIndex;
        1. 在擴容之前,transferIndex 在陣列的最右邊 。此時有一個執行緒發現已經到達擴容閾值,準備開始擴容。

        2. 擴容執行緒,在遷移資料之前,首先要將transferIndex左移(以cas的方式修改 transferIndex=transferIndex-stride(要遷移hash桶的個數)),獲取遷移任務。每個擴容執行緒都會通過for迴圈+CAS的方式設定transferIndex,因此可以確保多執行緒擴容的併發安全。(
          換個角度,我們可以將待遷移的table陣列,看成一個任務佇列,transferIndex看成任務佇列的頭指標。而擴容執行緒,就是這個佇列的消費者。擴容執行緒通過CAS設定transferIndex索引的過程,就是消費者從任務佇列中獲取任務的過程。
          )
      2.  擴容過程:
        1.  容量已經達到擴容閾值,需要進行擴容操作,此時transferindex=tab.length=32
        2. 擴容執行緒A 以cas的方式修改transferindex=31-16=16 ,然後按照降序遷移table[31]--table[16]這個區間的hash桶
        3. 遷移hash桶時,會將桶內的連結串列或者紅黑樹,按照一定演算法,拆分成2份,將其插入nextTable[i]和nextTable[i+n](n是table陣列的長度)。 遷移完畢的hash桶,會被設定成ForwardingNode節點,以此告知訪問此桶的其他執行緒,此節點已經遷移完畢
        4. 此時執行緒2訪問到了ForwardingNode節點,如果執行緒2執行的put或remove等寫操作,那麼就會先幫其擴容。如果執行緒2執行的是get等讀方法,則會呼叫ForwardingNode的find方法,去nextTable裡面查詢相關元素
        5. 執行緒2加入擴容操作
        6. 如果準備加入擴容的執行緒,發現以下情況,放棄擴容,直接返回。
          1. 發現transferIndex=0,即所有node均已分配

          2. 發現擴容執行緒已經達到最大擴容執行緒數

                                                    

      1.  原始碼解析
        private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
            int n = tab.length, stride;
            //先判斷CPU核數,如果是多核,將陣列長度/8,再/核數,得到stride,否則stride=陣列長度,如果stride<16,則stride=16
            //這裡的目的是讓每個CPU處理的桶一樣多,避免出現轉移任務不均勻的現象,如果桶較少的話,預設一個CPU(一個執行緒)處理16個桶,即確保每次至少獲取16個桶(遷移任務)
            if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
                stride = MIN_TRANSFER_STRIDE; // subdivide range
            //未初始化進行初始化
            if (nextTab == null) {            // initiating
                try {
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];//擴容2倍
                    nextTab = nt;//更新
                } catch (Throwable ex) {      // try to cope with OOME
                    sizeCtl = Integer.MAX_VALUE;//擴容失敗,sizeCtl使用int最大值。
                    return;
                }
                nextTable = nextTab;//更新成員變數
                //transferIndex預設=table.length
                transferIndex = n;
            }
            int nextn = nextTab.length;//新tab的長度
            //建立一個fwd節點,用於佔位。當別的執行緒發現這個槽位中是fwd型別的節點,表示其他執行緒正在擴容,並且此節點已經擴容完畢,跳過這個節點。關聯了nextTab,可以通過ForwardingNode.find()訪問已經遷移到nextTab的資料。
            ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
            //首次推進為 true,如果等於true,說明需要再次推進一個下標(i--),反之,如果是false,那麼就不能推進下標,需要將當前的下標處理完畢才能繼續推進
            boolean advance = true;
            //完成狀態,如果是true,就結束此方法。
            boolean finishing = false; // to ensure sweep before committing nextTab
            //自旋,i表示當前執行緒可以處理的當前桶區間最大下標,bound表示當前執行緒可以處理的當前桶區間最小下標
            for (int i = 0, bound = 0;;) {
                Node<K,V> f; int fh;
                 //while:如果當前執行緒可以向後推進;這個迴圈就是控制i遞減。同時,每個執行緒都會進入這裡取得自己需要轉移的桶的區間
                //分析場景:table.length=32,此時執行到這個地方nextTab.length=64 A,B執行緒同時進行擴容。
                //A,B執行緒同時執行到while迴圈中cas這段程式碼
                //A執行緒獲第一時間搶到資源,設定bound=nextBound=16,i = nextIndex - 1=31 A執行緒搬運table[31]~table[16]中間16個元素
                //B執行緒再次回到while起點,然後在次獲取到 bound = nextBound-0,i=nextIndex - 1=15,B執行緒搬運table[15]~table[0]中間16個元素
                //當transferIndex=0的時候,說明table裡面所有搬運任務都已經完成,無法在分配任務。
                while (advance) {
                    int nextIndex, nextBound;
                    // 對i減1,判斷是否大於等於bound(正常情況下,如果大於bound不成立,說明該執行緒上次領取的任務已經完成了。那麼,需要在下面繼續領取任務)
                    // 如果對i減1大於等於 bound,或者完成了,修改推進狀態為 false,不能推進了。任務成功後修改推進狀態為 true。
                    // 通常,第一次進入迴圈,i-- 這個判斷會無法通過,從而走下面的nextIndex = transferIndex(獲取最新的轉移下標)。其餘情況都是:如果可以推進,將i減1,然後修改成不可推進。如果i對應的桶處理成功了,改成可以推進。
                    if (--i >= bound || finishing)
                        advance = false;//這裡設定false,是為了防止在沒有成功處理一個桶的情況下卻進行了推進
                   // 這裡的目的是:1. 當一個執行緒進入時,會選取最新的轉移下標。
                   //             2. 當一個執行緒處理完自己的區間時,如果還有剩餘區間的沒有別的執行緒處理,再次CAS獲取區間。
                    else if ((nextIndex = transferIndex) <= 0) {
                        // 如果小於等於0,說明沒有區間可以獲取了,i改成-1,推進狀態變成false,不再推進
                        // 這個-1會在下面的if塊裡判斷,從而進入完成狀態判斷
                        i = -1;
                        advance = false;//這裡設定false,是為了防止在沒有成功處理一個桶的情況下卻進行了推進
                    }
                    // CAS修改transferIndex,即 length - 區間值,留下剩餘的區間值供後面的執行緒使用
                    else if (U.compareAndSwapInt
                             (this, TRANSFERINDEX, nextIndex,
                              nextBound = (nextIndex > stride ?
                                           nextIndex - stride : 0))) {
                        bound = nextBound;//這個值就是當前執行緒可以處理的最小當前區間最小下標
                        i = nextIndex - 1;//初次對i賦值,這個就是當前執行緒可以處理的當前區間的最大下標
                        advance = false;// 這裡設定false,是為了防止在沒有成功處理一個桶的情況下卻進行了推進,這樣導致漏掉某個桶。下面的 if(tabAt(tab, i) == f) 判斷會出現這樣的情況。
                    }
                }
                //i<0(不在 tab 下標內,按照上面的判斷,領取最後一段區間的執行緒結束)
                if (i < 0 || i >= n || i + n >= nextn) {
                    int sc;
                    if (finishing) {// 如果完成了擴容和資料遷移
                        nextTable = null;//刪除成員遍歷
                        table = nextTab;//更新table
                        sizeCtl = (n << 1) - (n >>> 1);//更新閥值
                        return;//結束transfer
                    }
                    //如果沒完成,嘗試將sc -1. 表示這個執行緒結束幫助擴容了,將 sc 的低 16 位減一。
                    if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                        //如果 sc - 2 不等於識別符號左移 16 位。如果他們相等了,說明沒有執行緒在幫助他們擴容了。也就是說,擴容結束了。
                        /**
                         *第一個擴容的執行緒,執行transfer方法之前(helpTransfer方法中),會設定 sizeCtl = (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2)
                         *後續幫其擴容的執行緒,執行transfer方法之前,會設定 sizeCtl = sizeCtl+1
                         *每一個退出transfer的方法的執行緒,退出之前,會設定 sizeCtl = sizeCtl-1
                         *那麼最後一個執行緒退出時:
                         *必然有sc == (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2),即 (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT
                        */
                        if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                            return;// 不相等,說明不到最後一個執行緒,直接退出transfer方法
                        finishing = advance = true;// 如果相等,擴容結束了,更新 finising 變數
                        i = n; // recheck before commit,最後退出的執行緒要重新check下是否全部遷移完畢
                    }
                }
                else if ((f = tabAt(tab, i)) == null) // 獲取老tab的i下標位置的變數,如果是 null,就使用 fwd 佔位。
                    advance = casTabAt(tab, i, null, fwd);// 如果成功寫入 fwd 佔位,再次推進一個下標
                else if ((fh = f.hash) == MOVED)// 如果不是 null 且 hash 值是 MOVED。
                    advance = true; // already processed,說明別的執行緒已經處理過了,再次推進一個下標
                else {// 到這裡,說明這個位置有實際值了,且不是佔位符。對這個節點上鎖。為什麼上鎖,防止 putVal 的時候向連結串列插入資料
                    synchronized (f) {
                        // 判斷 i 下標處的桶節點是否和 f 相同
                        if (tabAt(tab, i) == f) {
                            Node<K,V> ln, hn;// low, height 高位桶,低位桶
                            // 如果 f 的 hash 值大於 0 。TreeBin 的 hash 是 -2
                            if (fh >= 0) {
                                // 對老長度進行與運算(第一個運算元的的第n位於第二個運算元的第n位如果都是1,那麼結果的第n為也為1,否則為0)
                                // 由於 Map 的長度都是 2 的次方(000001000 這類的數字),那麼取於 length 只有 2 種結果,一種是 0,一種是1
                                //  如果是結果是0 ,Doug Lea 將其放在低位,反之放在高位,目的是將連結串列重新 hash,放到對應的位置上,讓新的取於演算法能夠擊中他。
                                int runBit = fh & n;
                                Node<K,V> lastRun = f; // 尾節點,且和頭節點的 hash 值取於不相等
                                // 遍歷這個桶
                                for (Node<K,V> p = f.next; p != null; p = p.next) {
                                    // 取於桶中每個節點的 hash 值
                                    int b = p.hash & n;
                                    // 如果節點的 hash 值和首節點的 hash 值取於結果不同
                                    if (b != runBit) {
                                        runBit = b; // 更新 runBit,用於下面判斷 lastRun 該賦值給 ln 還是 hn。
                                        lastRun = p; // 這個 lastRun 保證後面的節點與自己的取於值相同,避免後面沒有必要的迴圈
                                    }
                                }
                                if (runBit == 0) {// 如果最後更新的 runBit 是 0 ,設定低位節點
                                    ln = lastRun;
                                    hn = null;
                                }
                                else {
                                    hn = lastRun; // 如果最後更新的 runBit 是 1, 設定高位節點
                                    ln = null;
                                }// 再次迴圈,生成兩個連結串列,lastRun 作為停止條件,這樣就是避免無謂的迴圈(lastRun 後面都是相同的取於結果)
                                for (Node<K,V> p = f; p != lastRun; p = p.next) {
                                    int ph = p.hash; K pk = p.key; V pv = p.val;
                                    // 如果與運算結果是 0,那麼就還在低位
                                    if ((ph & n) == 0) // 如果是0 ,那麼建立低位節點
                                        ln = new Node<K,V>(ph, pk, pv, ln);
                                    else // 1 則建立高位
                                        hn = new Node<K,V>(ph, pk, pv, hn);
                                }
                                // 其實這裡類似 hashMap
                                // 設定低位連結串列放在新陣列的 i
                                setTabAt(nextTab, i, ln);
                                // 設定高位連結串列,在原有長度上加 n
                                setTabAt(nextTab, i + n, hn);
                                // 將舊的連結串列設定成佔位符,表示處理過了
                                setTabAt(tab, i, fwd);
                                // 繼續向後推進
                                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);
                                    // 和連結串列相同的判斷,與運算 == 0 的放在低位
                                    if ((h & n) == 0) {
                                        if ((p.prev = loTail) == null)
                                            lo = p;
                                        else
                                            loTail.next = p;
                                        loTail = p;
                                        ++lc;
                                    } // 不是 0 的放在高位
                                    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);
                                // 高位數
                                setTabAt(nextTab, i + n, hn);
                                // 舊的設定成佔位符
                                setTabAt(tab, i, fwd);
                                // 繼續向後推進
                                advance = true;
                            }
                        }
                    }
                }
            }
        }
    1. addCount:計數

      // 從 putVal 傳入的引數是x=1,check=binCount預設是0,只有hash衝突了才會大於1,且他的大小是連結串列的長度(如果不是紅黑樹結構的話,紅黑樹=2)。
          private final void addCount(long x, int check) {
              CounterCell[] as; long b, s;
               //如果計數盒子不是空或者修改 baseCount 失敗
              if ((as = counterCells) != null ||
                  !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
                  CounterCell a; long v; int m;
                  boolean uncontended = true;
                   // 如果計數盒子是空(尚未出現併發)
                   // 如果隨機取餘一個陣列位置為空 或者
                   // 修改這個槽位的變數失敗(出現併發了)
                   // 執行 fullAddCount 方法,在fullAddCount自旋直到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();
              }
              // 檢查是否需要擴容,在 putVal 方法呼叫時,預設就是要檢查的(check預設是0,連結串列是連結串列長度,紅黑樹是2),如果是值覆蓋了,就忽略
              if (check >= 0) {
                  Node<K,V>[] tab, nt; int n, sc;
                  // 如果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 減一。這個時候,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;
                          // 不滿足前面5個條件時,嘗試參與此次擴容,把正在執行transfer任務的執行緒數加1,+2代表有1個,+1代表有0個,表示多了一個執行緒在幫助擴容,執行transfer
                          if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                              transfer(tab, nt);
                      }
                      //如果不在擴容,將 sc 更新:識別符號左移 16 位 然後 + 2. 也就是變成一個負數。高 16 位是識別符號,低 16 位初始是 2.
                      //試著讓自己成為第一個執行transfer任務的執行緒
                      else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                                   (rs << RESIZE_STAMP_SHIFT) + 2))
                          transfer(tab, null);
                      s = sumCount();// 重新計數,判斷是否需要開啟下一輪擴容
                  }
              }
          }
  1. get:

    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());
            //table有值,且查詢到的槽位有值(tabAt方法通過valatile讀)
            if ((tab = table) != null && (n = tab.length) > 0 &&
                (e = tabAt(tab, (n - 1) & h)) != null) {
                //hash、key、value都相同返回當前查詢到節點的值
                if ((eh = e.hash) == h) {
                    if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                        return e.val;
                }
                //遍歷特殊節點:紅黑樹、已經遷移的節點(ForwardingNode)等
                else if (eh < 0)
                    return (p = e.find(h, key)) != null ? p.val : null;
                //遍歷node連結串列(e.next也是valitle變數)
                while ((e = e.next) != null) {
                    if (e.hash == h &&
                        ((ek = e.key) == key || (ek != null && key.equals(ek))))
                        return e.val;
                }
            }
            return null;
        }
        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;
        }
  2. remove:

    public V remove(Object key) {
            return replaceNode(key, null, null);
        }
        //通過volatile設定第i個節點的值
        static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
            U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
        }
        final V replaceNode(Object key, V value, Object cv) {
            int hash = spread(key.hashCode());
            //自旋
            for (Node<K,V>[] tab = table;;) {
                Node<K,V> f; int n, i, fh;
                //陣列或查詢的槽位為空,結束自旋返回null
                if (tab == null || (n = tab.length) == 0 ||
                    (f = tabAt(tab, i = (n - 1) & hash)) == null)
                    break;
                //正在擴容,幫助擴容
                else if ((fh = f.hash) == MOVED)
                    tab = helpTransfer(tab, f);
                else {
                    V oldVal = null;//返回的舊值
                    boolean validated = false;//是否進行刪除連結串列或紅黑樹節點
                    synchronized (f) {//槽位加鎖
                        //getObjectVolatile獲取tab[i],如果此時tab[i]!=f,說明其他執行緒修改了tab[i]。回到for迴圈開始處,重新執行
                        if (tabAt(tab, i) == f) {//槽位節點沒有變化
                            if (fh >= 0) {//槽位節點是連結串列
                                validated = true;
                                //遍歷連結串列
                                for (Node<K,V> e = f, pred = null;;) {
                                    K ek;
                                    //hash、key、value相同
                                    if (e.hash == hash &&
                                        ((ek = e.key) == key ||
                                         (ek != null && key.equals(ek)))) {
                                        V ev = e.val;//臨時節點快取當前節點值
                                        //值相同
                                        if (cv == null || cv == ev ||
                                            (ev != null && cv.equals(ev))) {
                                            oldVal = ev;//給舊值賦值
                                            if (value != null)//值覆蓋,replace()呼叫
                                                e.val = value;
                                            else if (pred != null)//有前節點,表示當前節點不是頭節點
                                                pred.next = e.next;//刪除當前節點
                                            else
                                                setTabAt(tab, i, e.next);//刪除頭節點,即更新當前槽位(陣列槽位)節點為頭節點的下一節點
                                        }
                                        break;
                                    }
                                    //當前節點不是目標節點,繼續遍歷下一個節點
                                    pred = e;
                                    //到達連結串列尾部,依舊沒有找到,跳出迴圈
                                    if ((e = e.next) == null)
                                        break;
                                }
                            }
                            else if (f instanceof TreeBin) {//紅黑樹
                                validated = true;
                                TreeBin<K,V> t = (TreeBin<K,V>)f;
                                TreeNode<K,V> r, p;
                                //樹有節點且查詢的節點不為null
                                if ((r = t.root) != null &&
                                    (p = r.findTreeNode(hash, key, null)) != null) {
                                    V pv = p.val;
                                    //值相同
                                    if (cv == null || cv == pv ||
                                        (pv != null && cv.equals(pv))) {
                                        oldVal = pv;//給舊值賦值
                                        if (value != null)//值覆蓋,replace()呼叫
                                            p.val = value;
                                        else if (t.removeTreeNode(p))//刪除節點成功
                                            setTabAt(tab, i, untreeify(t.first));//更新當前槽位(陣列槽位)節點為樹的第一個節點
                                    }
                                }
                            }
                        }
                    }
                    if (validated) {
                        //如果刪除了節點,更新size
                        if (oldVal != null) {
                            if (value == null)
                                addCount(-1L, -1);//數量-1
                            return oldVal;
                        }
                        break;
                    }
                }
            }
            return null;
        }

  三.總結

  1.  put:使用cas插入,如果是連結串列或樹節點才會加鎖同步操作,提高了效能

    1. 不允許有key或value為null,否則丟擲異常;
    2. 在第一次put時初始化table(initTable()),初始化有併發控制,通過sizeCtl變數判斷,sizeCtl<0表示已經有執行緒在初始化,當前執行緒就不在進行,否則sizeCtl置為-1(CAS)並建立陣列;
    3. 當hash計算出的槽位節點為null時,使用CAS插入元素;
    4. 當hash為MOVED(-1)時,幫助擴容,但可能幫助不了,因為每個執行緒預設16個桶,如果只有16個桶,第二個執行緒無法幫助擴容;
    5. 如果hash衝突了,同步槽位節點,如果槽位是連結串列結構,進行連結串列操作,覆蓋舊值或插入到連結串列尾部;如果是樹結構,新增到樹中;
    6. 元素新增到連結串列或樹中,如果連結串列長度大於8,將連結串列轉換為紅黑樹;
    7. 呼叫addCount(),對size+1,並判斷是否需要擴容addCount(),如果是值覆蓋操作就不需要呼叫該方法;
  2. initTable:初始化

    1. 陣列table為null才進行初始化,否則直接返回舊陣列;
    2. 如果當前sizeCtl小於0,表示有執行緒正在初始化,則當前執行緒禮讓CPU,保證只有一個執行緒正在初始化陣列;
    3. 如果沒有執行緒在初始化,則當前執行緒CAS將sizeCtl置為-1並建立陣列,然後重新計算閥值;
  3. helpTransfer:幫助擴容

    1. 當嘗試插入操作時,發現節點是forward型別,則會幫助擴容;
    2. 每次加入一個執行緒都會將sizeCtl的低16位+1,同時校驗高16位的識別符號;
    3. 擴容最大的幫助執行緒是65535,這是低16位的最大值限制;
    4. 每個執行緒預設分配16個桶,如果桶的數量是16,那麼第二個執行緒無法幫助擴容,即桶被分配完其他執行緒無法進場擴容;
  4. transfer:擴容和資料遷移

    1. 根據CPU核數平均分配給每個CPU相同數量的桶,如果不夠16個,預設就是16個;
    2. 按照2倍容量進行擴容;
    3. 每個執行緒在處理完自己領取的區間後,還可以繼續領取,如果還有的話,通過transferIndex變數遞減16實現桶數量控制;
    4. 每次處理空桶的時候,會把當前桶標識為forward節點,告訴put的其他執行緒說“我正在擴容,快來幫忙”,但如果只有16個桶,只能有一個執行緒進行擴容;
    5. 如果有了佔位符MOVED,表示已經被處理過,跳過這個桶,繼續推進處理其他桶;
    6. 如果有真正的實際值,那麼就同步加鎖頭節點,防止putVal的併發;
    7. 同步塊裡將連結串列拆分成兩份,根據 hash & length 得到是否是0,如果是0,放在新陣列低位,反之放在length+i的高位。這是防止下次取值hash找不到正確的位置;
    8. 如果該桶型別是紅黑樹,也會拆分成2個,然後判斷拆分過的桶的大小是否小於等於6,如果是轉換成連結串列;
    9. 執行緒處理完如果沒有可選區間,且任務沒有完成,則會將整個表檢查一遍,防止遺漏;
  5. addCount:擴容判斷

    1. 當插入結束時,會對size+1,並判斷是否需要擴容的判斷;
    2. 優先使用計數盒子(如果不是空,說明併發了),如果計數盒子是空,使用baseCount變數+1;
    3. 如果修改baseCount失敗,使用計數盒子,如果還是修改失敗,在fullAddCount()中自旋直到CAS操作成功;
    4. 檢查是否需要擴容;
    5. 如果size大於等於sizeCtl且長度小於1<<30,可以擴容;
    6. 如果已經在擴容,幫助其擴容;
    7. 如果沒有在擴容,自行開啟擴容,更新sizeCtl變數為負數,賦值為識別符號高16位+2;
  6. remove:刪除元素

    1. 自旋遍歷數量,如果陣列或根據hash計算的槽位節點值為null,直接結束自旋返回null;
    2. 如果槽位節點正在擴容,幫助擴容;
    3. 如果槽位節點有值,同步加鎖;
    4. 如果該槽位節點還是沒有任何變化,判斷是連結串列結構型別節點還是樹結構型別節點,通過遍歷查詢元素,找到刪除該節點或重新設定頭節點;
    5. 如果刪除了節點,更新size-1,如果有舊值則返回舊值,否則返回null;

  四.參考

  1. https://www.jianshu.com/p/2829fe36a8dd
  2. https://www.jianshu.com/p/487d00afe6ca
  3. https://juejin.im/post/5b001639f265da0b8f62d0f8#comment

相關文章