JAVA多線程的初級認識7-ConcurrentHashMap簡要分析

CHM1.7和1.8的更改

網上看了很多CHM的分析,可能都不是很讓我能有恍然大明白的感覺,畢竟1.7到1.8的巨大改動,肯定是有什麼更高深的算法或者設計在裏面。所以想自己去分析下。個人愚見,不喜歡請輕噴。

看了JDK1.8的CHM的代碼,洋洋灑灑6000+行,應該是jdk源碼最複雜的一個類了吧。頭疼,分析一下流程和核心API吧~

對比1.8和1.7的升級優化的區別如下:

  • 1.7時候是CHM是多個Segment (默認應該是16個),Segment集成ReentrantLock來實現鎖定。所以鎖的粒度寬泛。每個segment對應一個數組+鏈表的組合。理論上支持最高16個線程併發寫入。
  • 1.8時候CHM鎖是針對數組單個元素,鎖的粒度更加細微,衝突概率更低,效率更高。
  • 1.8時候當鏈表過長的時候會升級爲紅黑樹。防止hash攻擊等降低查詢效率
  • 1.7結構如圖
    在這裏插入圖片描述
    CHM1.8同時也維護了1.7的segment

CHM核心類和關鍵成員變量

Node

普通的數據節點,維護了hash, key , value, nextNode的屬性

TreeNode

紅黑樹的數據節點,維護了hash, key , value, nextNode的屬性

TreeBin

紅黑樹的根節點。沒實際數據意義,hash = -2

ForwardingNode

當map需要reszie的時候,放在鏈表頭的節點。沒實際數據意義,hash = -1

ReservationNode

當map是computeIfAbsent時候用於保留,hash = -3

sizeCtl

/**
 * Table initialization and resizing control.  When negative, the
 * table is being initialized or resized: -1 for initialization,
 * else -(1 + the number of active resizing threads).  Otherwise,
 * when table is null, holds the initial table size to use upon
 * creation, or 0 for default. After initialization, holds the
 * next element count value upon which to resize the table.
 */
private transient volatile int sizeCtl;

主要用途是用來標誌現在map的狀態的。

  • 0 default
  • -1 初始化
  • -(1 + resize線程數) map正在resize,並且可以多線程resize
  • >0 map下次resize的大小

CounterCell

/**
 * A padded cell for distributing counts.  Adapted from LongAdder
 * and Striped64.  See their internal docs for explanation.
 */
@sun.misc.Contended static final class CounterCell {
    volatile long value;
    CounterCell(long x) { value = x; }
}

精髓之一

用於計算totalSize,當併發過大的時候,如果通過CAS來進行同步,當然可以,但是效率很低。所以在計算totalSize的時候是通過baseCount和CountCell[]來計算的,運用了分流治之的思想。

CHM核心API分析

put

源碼如下:

/**
 * Maps the specified key to the specified value in this table.
 * Neither the key nor the value can be null.
 *
 * <p>The value can be retrieved by calling the {@code get} method
 * with a key that is equal to the original key.
 *
 * @param key key with which the specified value is to be associated
 * @param value value to be associated with the specified key
 * @return the previous value associated with {@code key}, or
 *         {@code null} if there was no mapping for {@code key}
 * @throws NullPointerException if the specified key or value is null
 */
public V put(K key, V value) {
    return putVal(key, value, false);
}

/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    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
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            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;
                            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) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

雖然主函數只有不到100行-。-可是各個地方都透露着精髓~

初始化

第一個if:初始化

/**
 * Initializes table, using the size recorded in sizeCtl.
 */
private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        if ((sc = sizeCtl) < 0)
            Thread.yield(); // lost initialization race; just spin
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                if ((tab = table) == null || tab.length == 0) {
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    sc = n - (n >>> 2);
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

主要是通過對於sizeCtl 的判斷和cas進行初始化工作,哪個線程搶到了sizeCtl(這個變量是volatile的 ),哪個線程進行初始化。其中CAS是通過Unsafe類進行操作。

設置數組節點

第二個if是當前下標數組爲null,通過cas setValue

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
}
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);
}

這個地方有一個知識點,既然table是volatile的,爲什麼還要用Unsafe類進行cas呢?

因爲volatile只是對對象引用是線程可見的,對內部元素不是!

拓容

第三個if 判斷是否在resize

else if ((fh = f.hash) == MOVED)
    tab = helpTransfer(tab, f);

resize也是CHM的精髓之一

  • 首先1.8的CHM支持多線程拓容
  • 使用CAS來實現無鎖化的並行拓容,到底有多犀利!還得慢慢看doug Lea大神的思想

廢話不多說,先上源碼:

    /**
     * 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;
        if (tab != null && (f instanceof ForwardingNode) &&
            (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
            int rs = resizeStamp(tab.length);
            while (nextTab == nextTable && table == tab &&
                   (sc = sizeCtl) < 0) {
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || transferIndex <= 0)
                    break;
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                    transfer(tab, nextTab);
                    break;
                }
            }
            return nextTab;
        }
        return table;
    }
  • 判斷是否需要幫助 —>看nextTable是否已經完成

    • 完成則直接退出
    • 沒完成則生成自己線程的Stamp進場幫忙resize
    • 這裏用到了sizeCtl 多一個線程幫忙拓容,則通過CAS對sizeCtl + 1
    • 具體看看如何多線程無鎖拓容的transfer函數
  • 前提知識點:自己線程的stamp是什麼?(resizeStamp函數)

    • 		/**
           * The number of bits used for generation stamp in sizeCtl.
           * Must be at least 6 for 32bit arrays.
           */
          private static int RESIZE_STAMP_BITS = 16;
      
          /**
           * The maximum number of threads that can help resize.
           * Must fit in 32 - RESIZE_STAMP_BITS bits.
           */
          private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
      
          /**
           * The bit shift for recording size stamp in sizeCtl.
           */
          private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
      		/**
           * Returns the stamp bits for resizing a table of size n.
           * Must be negative when shifted left by RESIZE_STAMP_SHIFT.
           */
          static final int resizeStamp(int n) {
              return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
          }
      
      • 首先拓容線程stamp和源table的長度有關~

        比如n = 16 ; 二進制 —> 0000 0000 0000 0000 0000 0000 0001 0000

        Interger.numberofLeadingZeros(16) = 27 (返回無符號數第一個不爲0數字前有多少個0)

        27的二進制: 0000 0000 0000 0000 0000 0000 0001 1100

        resizeStamp(16) 結果就是 —> 0000 0000 0000 0000 1000 0000 0001 1100

        ok 到此爲止,線程的拓容戳已經創建好了~

  • 前提知識點:sizeCtl的resize更新

    • 第一次初始化resize值 ( 1 + resize的線程數)

    • else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                                   (rs << RESIZE_STAMP_SHIFT) + 2))
      
    • 隨後增加線程幫助拓容 + 1

    • if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
      
    • 第一次sizeCtl更新爲(如果table長度是16)

      • rs = 0000 0000 0000 0000 1000 0000 0001 1100
      • (rs << RESIZE_STAMP_SHIFT) + 2)) = 1000 0000 0001 1100 0000 0000 0000 0010
      • 10進製爲-2145648638
      • 則sizeCtl則爲這個值-2145648638
    • 再有線程進入幫忙拓容則在+1

      • 則sizeCtl則+1
      • 則sizeCtl = -2145648638 + 1 = -2145648637 = 1000 0000 0001 1100 0000 0000 0000 0011
      • 所以不用注重10進制數值,僅注意2進制即可
    • 高16位 低16位
      拓容標記 並行拓容的線程數量
  • transfer函數

    簡單來說,就是將原有table通過指針劃分成多個區間,然後各個線程負責自己的區間。每個區間的數組按照逆向遍歷的方式進行遷移,前已完成的數組下標的元素會標記爲ForwardingNode,表示該數組下標已經拓容完成。不多說先上源碼(以table.size = 16爲例):

    		/** Number of CPUS, to place bounds on some sizings */
        static final int NCPU = Runtime.getRuntime().availableProcessors();
    		private static final int MIN_TRANSFER_STRIDE = 16;
    		/**
         * Moves and/or copies the nodes in each bin to new table. See
         * above for explanation.
         */
        private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
            int n = tab.length, stride;
            //計算每個線程負責區間的長度,通過當前機器的CPU和確定,如果小於16,則按照16來進行計算
            if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
                stride = MIN_TRANSFER_STRIDE; // subdivide range
            //初始化,構建 << 1長度的table
            if (nextTab == null) {            // initiating
                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;
          	//創建一個FowwardingNode,維護的是拓容後的數組。用來告知是否數組bucket已經拓容完成
            ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
          	//拓容是否推進,即是否拓容完了一個bucket,是否進行下一個
            boolean advance = true;
          	//節點是否已經拓容完畢
            boolean finishing = false; // to ensure sweep before committing nextTab
          	
            for (int i = 0, bound = 0;;) {
              	//通過循環來處理bucket中的元素,通過CAS設置transferIndex,循環中i表示正在處理的bucket位置,bound表示需要處理的邊界,初始化transferIndex = 16.
                Node<K,V> f; int fh;
                while (advance) {
                    int nextIndex, nextBound;
                  	//i = -1;
                  	// --i保證了正在處理的bucket向下的遍歷
                    if (--i >= bound || finishing)
                        advance = false;
                  	//nextIndex = 16;
                    else if ((nextIndex = transferIndex) <= 0) {
                        i = -1;
                        advance = false;
                    }
                  	//transferIndex = 16;
                  	//nextBound = 0;
                    else if (U.compareAndSwapInt
                             (this, TRANSFERINDEX, nextIndex,
                              nextBound = (nextIndex > stride ?
                                           nextIndex - stride : 0))) {
                      	//bound = 0;
                      	//i = 15;
                        bound = nextBound;
                        i = nextIndex - 1;
                        advance = false;
                    }
                }
              	//經過分配後,當前線程的處理[0,15],transferIndex = 0;
              	// 當完成了當前線程的任務
                if (i < 0 || i >= n || i + n >= nextn) {
                    int sc;
                  	// 拓容完成,更新成員變量
                    if (finishing) {
                        nextTable = null;
                        table = nextTab;
                        sizeCtl = (n << 1) - (n >>> 1);
                        return;
                    }
                  	// 當前線程拓展完畢,則更新sizeCtl - 1
                    if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                      	// 全部拓容線程更新完畢,則執行return
                        if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                            return;
                      	// 更新當前線程標記量
                        finishing = advance = true;
                      	// i = 16
                        i = n; // recheck before commit
                    }
                }
              	// 如果當前老table的i = 15 位置爲空,則放一個fwd
                else if ((f = tabAt(tab, i)) == null)
                    advance = casTabAt(tab, i, null, fwd);
              	// 已經處理過的bucket
                else if ((fh = f.hash) == MOVED)
                    advance = true; // already processed
              	// 如果當前位置老table的i不爲Null,首先鎖定鏈表首節點
                else {
                    synchronized (f) {
                        if (tabAt(tab, i) == f) {
                            Node<K,V> ln, hn;
                            if (fh >= 0) {
                                int runBit = fh & n;//ln 表示低位, hn 表示高位;接下來這段代碼的作用 是把鏈表拆分成兩部分,0 在低位,1 在高位
                                Node<K,V> lastRun = f; //鏈表頭
                                for (Node<K,V> p = f.next; p != null; p = p.next) {
                                  	// n = 16, 二進制是 1 0000, & 表示看p.hash的第5位是否爲1
                                    int b = p.hash & n;
                                    if (b != runBit) {
                                        runBit = b;
                                        lastRun = p;
                                    }
                                }
                              	// 開始一直不明白爲什麼需要加這個循環,直接區分高低位,形成兩個新的鏈表即可,後來仔細研究才明白,配合後面的循環,目的是爲了如果尾部的runbit都一致,那就沒必要在進行重新構造。
                                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);
                                }
                              	// 鏈表插入到對應的位置,並且更新老table當前i的fwd-->已經遷移完成
                                setTabAt(nextTab, i, ln);
                                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);
                                    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. 計算區間大小 = 16;

    2. 初始化,如果原大小是16的話。
      在這裏插入圖片描述

    3. 計算當前線程所處理的區間。如果是32拓展到64的話如圖:
      在這裏插入圖片描述

    4. 遍歷各個線程負責的區間,根據i來處理對應的bucket.

      1. 原table第15個bucket是null
        在這裏插入圖片描述

      2. 直接將老的table設置爲fwd.處理完成後如下:
        在這裏插入圖片描述

      3. 繼續遍歷,一直到老table中一個不爲null的bucket,如圖:
        在這裏插入圖片描述

      4. 遍歷鏈表計算runbit
        在這裏插入圖片描述

      5. 構建新的高低鏈表
        在這裏插入圖片描述

      6. 將新的高低鏈表插入到新的nextTable當中
        在這裏插入圖片描述

      7. 更新老的table,把i的bucket設置爲fwd
        在這裏插入圖片描述

      8. 當所有節點都fwd後,設置finishing,進行退出拓容操作。sizeCtl - 1,如果所有線程已經拓容完畢,則會判斷rs和sizeCtl的比較,重新設置sizeCtl~如下代碼:

			if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                if (finishing) {
                    nextTable = null;
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }

總結:

充分利用了CAS和併發的效率,從而可以高效的進行拓容操作。

設置鏈表節點

最後是要遍歷鏈表或者數組進行SetValue

else {
    V oldVal = null;
    synchronized (f) {
        if (tabAt(tab, i) == f) {
            if (fh >= 0) {
                binCount = 1;
                for (Node<K,V> e = f;; ++binCount) {
                    K ek;
                    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;
                    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) {
        if (binCount >= TREEIFY_THRESHOLD)
            treeifyBin(tab, i);
        if (oldVal != null)
            return oldVal;
        break;
    }
}

首先鎖住鏈表的head,防止其他線程進入,進入單線程模式~(這裏也就是1.7和1.8鎖的粒度不一致的地方了,1.8更高效,因爲鎖的粒度更低

之後就是遍歷鏈表或者紅黑樹,和hashmap將數據放進去即可~

最後判斷一下數組長度,如果是大於紅黑樹閾值,就轉化爲紅黑樹數據結構。

AddCount

最後是重中之重 ,put了一個元素進去,該增加size了~

   /**
     * Adds to count, and if table is too small and not already
     * resizing, initiates transfer. If already resizing, helps
     * perform transfer if work is available.  Rechecks occupancy
     * after a transfer to see if another resize is already needed
     * because resizings are lagging additions.
     *
     * @param x the count to add
     * @param check if <0, don't check resize, if <= 1 only check if uncontended
     */
    private final void addCount(long x, int check) {
        CounterCell[] as; long b, s;
        if ((as = counterCells) != null ||
            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            CounterCell a; long v; int m;
            boolean uncontended = true;
            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;
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (n = tab.length) < MAXIMUM_CAPACITY) {
                int rs = resizeStamp(n);
                if (sc < 0) {
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
                s = sumCount();
            }
        }
    }

流程圖如下:

第一步更新baseCount,通過CAS更新失敗,則說明存在併發,那麼走第二個if

在這裏插入圖片描述

第二步獲取隨機數,然後計算到底這個count應該加再哪個CounterCell數組中,然後通過cas添加。如果失敗,則說明存在競爭,進入fullAddCount方法

需要注意兩個地方:

  • ThreadLocalRandom這個類繼承於Random,適合併發場景。這裏面隨機數是爲了尋找隨機的數組下標,減小衝突。而該類會比Randon減少衝突的次數。
  • 這裏面和正常map尋找數組下標一樣,也是用&而不是用%。
    在這裏插入圖片描述

第三步 fullAddCount方法我放棄了。簡要說下思路:

  • 初始化CounterCell size = 2並且以<<1 拓容

  • 本身方法是一個自旋

  • 使用到了自旋鎖

        /**
         * Spinlock (locked via CAS) used when resizing and/or creating CounterCells.
         */
        private transient volatile int cellsBusy;
    

總結下爲什麼要這麼做?把這個addCount方法設計的這麼複雜。

  • 不直接使用synchronized是爲了效率
  • 不直接使用CAS,是害怕在高併發的時候,不停在CAS丟失了效率
  • 使用baseCount + CounterCell[]的好處是在於
    • 低併發的時候可以直接用baseCount解決問題
    • 高併發的時候,可以通過ThreadLocalRandom和CounterCell[]進行很好的分流工作,有效的減少了無意義的鎖和CAS。類似於Nginx負載均衡的效果。

get

get相對來說比較簡單了直接尋找hash和key.equals上源碼:

  /**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     *
     * <p>More formally, if this map contains a mapping from a key
     * {@code k} to a value {@code v} such that {@code key.equals(k)},
     * then this method returns {@code v}; otherwise it returns
     * {@code null}.  (There can be at most one such mapping.)
     *
     * @throws NullPointerException if the specified key is null
     */
    public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        int h = spread(key.hashCode());
      	//正常尋找數組中的bucket位置
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
          	//hash相同的話,直接比較key
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
          	//bucket中的hash 是負數 -->可能在拓容,遍歷鏈表
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
          	//遍歷bucket的鏈表尋找元素
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

size

看過了put方法的addCount方法,其實這個就很簡單了,直接上源碼:

    /**
     * {@inheritDoc}
     */
    public int size() {
        long n = sumCount();
        return ((n < 0L) ? 0 :
                (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
                (int)n);
    }


    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;
    }

就是遍歷counterCell的值再累加baseCount即可~~

總結

  • 在鎖很重的時候,如果可以用volatile和CAS來巧妙的避開
  • 當併發大的時候,如果可以分而治之
  • 架構和細節的優化同樣重要
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章