關於jdk1.8中ConcurrentHashMap的方方面面

前言

Java JDK升級到1.8後有些集合類的實現有了變化,其中ConcurrentHashMap就有進行結構上的大調整。jdk1.6、1.7實現的共同點主要是通過採用分段鎖Segment減少熱點域來提高併發效率,1.8版本的實現有哪些變化呢?

重要概念

在正式研究前,我們需要先知道幾個重要參數,提前說明其值所代表的意義以便更好的講解源碼實現。

table

所有數據都存在table中,table的容量會根據實際情況進行擴容,table[i]存放的數據類型有以下3種:

TreeBin 用於包裝紅黑樹結構的結點類型

ForwardingNode 擴容時存放的結點類型,併發擴容的實現關鍵之一
Node 普通結點類型,表示鏈表頭結點

nextTable

擴容時用於存放數據的變量,擴容完成後會置爲null。

sizeCtl

以volatile修飾的sizeCtl用於數組初始化與擴容控制,它有以下幾個值:

當前未初始化:
	= 0  //未指定初始容量
	> 0  //由指定的初始容量計算而來,再找最近的2的冪次方。
		//比如傳入6,計算公式爲6+6/2+1=10,最近的2的冪次方爲16,所以sizeCtl就爲16。
初始化中:
	= -1 //table正在初始化
	= -N //N是int類型,分爲兩部分,高15位是新容量的值,低16位(M)表示
	         //並行擴容線程數+1,具體在resizeStamp函數介紹。
初始化完成:
	=table.length * 0.75  //擴容閾值調爲table容量大小的0.75倍

其它的分析相應源碼時再細說。

put()

	public V put(K key, V value) {
        return putVal(key, value, false);
    }

    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());   //@1,講解見下面小標題。
         //i處結點數量,2: TreeBin或鏈表結點數, 其它:鏈表結點數。主要用於每次加入結點後查看是否要由鏈表轉爲紅黑樹
        int binCount = 0; 
        for (Node<K,V>[] tab = table;;) {   //CAS經典寫法,不成功無限重試,讓再次進行循環進行相應操作。
            Node<K,V> f; int n, i, fh;
            //除非構造時指定初始化集合,否則默認構造不初始化table,所以需要在添加時元素檢查是否需要初始化。
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();  //@2
            //CAS操作得到對應table中元素
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { //@3
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   //null創建Node對象做爲鏈表首結點
            }
            else if ((fh = f.hash) == MOVED)  //當前結點正在擴容
            	//讓當前線程調用helpTransfer也參與到擴容過程中來,擴容完畢後tab指向新table。
                tab = helpTransfer(tab, f); 
            else {
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {  //雙重檢查i處結點未變化
                        if (fh >= 0) {  //表明是鏈表結點類型,hash值是大於0的,即spread()方法計算而來
                            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;
                                    //onlyIfAbsent表示是新元素才加入,舊值不替換,默認爲fase。
                                    if (!onlyIfAbsent)  
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
	                                //jdk1.8版本是把新結點加入鏈表尾部,next由volatile修飾
                                    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) {   //@4
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)  //默認桶中結點數超過8個數據結構會轉爲紅黑樹
                        treeifyBin(tab, i);   //@5
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);  //更新size,檢測擴容
        return null;
    }

註釋已說的比較明白,上面的代碼中的數字註釋再單獨細說下:

spread()

jdk1.8的hash策略,與以往版本一樣都是爲了減少hash衝突:

	static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash   //01111111_11111111_11111111_11111111
	
    static final int spread(int h) {
	    //無符號右移加入高位影響,與HASH_BITS做與操作保留對hash有用的比特位,有讓hash>0的意思
        return (h ^ (h >>> 16)) & HASH_BITS;
    }

initTable()

initTable()用於裏面table數組的初始化,值得一提的是table初始化是沒有加鎖的,那麼如何處理併發呢?
由下面代碼可以看到,當要初始化時會通過CAS操作將sizeCtl置爲-1,而sizeCtl由volatile修飾,保證修改對後面線程可見。
這之後如果再有線程執行到此方法時檢測到sizeCtl爲負數,說明已經有線程在給擴容了,這個線程就會調用Thread.yield()讓出一次CPU執行時間。

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(); 
                //正在初始化時將sizeCtl設爲-1
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;  //DEFAULT_CAPACITY爲16
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);   //擴容閾值爲新容量的0.75倍
                    }
                } finally {
                    sizeCtl = sc;   //擴容保護
                }
                break;
            }
        }
        return tab;
    }

tabAt()/casTabAt()/setTabAt()

ABASE表示table中首個元素的內存偏移地址,所以(long)i << ASHIFT) + ABASE得到table[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);
    }

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

    static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
        U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
    }

對i位置結點的寫操作有兩個方法,casTabAt()與setTabAt()。源碼中有這樣一段註釋:

     * Note that calls to setTabAt always occur within locked regions,
     * and so in principle require only release ordering, not
     * full volatile semantics, but are currently coded as volatile
     * writes to be conservative.

所以要原子語義的寫操作需要使用casTabAt(),setTabAt()是在鎖定桶的狀態下才會被調用,之所以實現成這樣只是帶保守性的一種寫法而已。放鬆一下繼續~
在這裏插入圖片描述

TreeBin

註釋4、5都是有關TreeBin的操作,爲進一步提升性能,ConcurrentHashMap引入了紅黑樹。
引入紅黑樹是因爲鏈表查詢的時間複雜度爲O(n),紅黑樹查詢的時間複雜度爲O(log(n)),所以在結點比較多的情況下使用紅黑樹可以大大提升性能。

紅黑樹是一種自平衡二叉查找樹,有如下性質:

  • 每個節點要麼是紅色,要麼是黑色。
  • 根節點永遠是黑色的。
  • 所有的葉節點都是空節點(即 null),並且是黑色的。
  • 每個紅色節點的兩個子節點都是黑色。(從每個葉子到- 根的路徑上不會有兩個連續的紅色節點)
  • 從任一節點到其每個葉子的所有簡單路徑都包含相同數目的黑色節點。

圖例:
在這裏插入圖片描述

treeifyBin()

桶內元素超時8個時會調用到此方法。

private final void treeifyBin(Node<K,V>[] tab, int index) {
        Node<K,V> b; int n, sc;
        if (tab != null) {
            if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
                tryPresize(n << 1);  //如果數組整體容量太小則去擴容,放棄轉紅黑樹結構
            else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
                synchronized (b) {
                    if (tabAt(tab, index) == b) {
                        TreeNode<K,V> hd = null, tl = null;
                        for (Node<K,V> e = b; e != null; e = e.next) {
                            TreeNode<K,V> p =
                                new TreeNode<K,V>(e.hash, e.key, e.val,
                                                  null, null);
                            if ((p.prev = tl) == null)
                                hd = p;
                            else
                                tl.next = p;
                            tl = p;
                        }
                        setTabAt(tab, index, new TreeBin<K,V>(hd));

可以看出按原Node鏈表順序轉爲了TreeNode鏈表,每個TreeNode的prev、next已完備,傳入頭結點hd構造紅黑樹。

TreeBin構造函數

TreeBin(TreeNode<K,V> b) {
			//Node(int hash, K key, V val, Node<K,V> next)
            super(TREEBIN, null, null, null);
            this.first = b;
            TreeNode<K,V> r = null;
            for (TreeNode<K,V> x = b, next; x != null; x = next) {  //依次處理每個結點
                next = (TreeNode<K,V>)x.next;
                x.left = x.right = null;
                if (r == null) {
                    x.parent = null;
                    x.red = false;  //根結點爲黑色
                    r = x;
                }
                else {
                    K k = x.key;
                    int h = x.hash;
                    Class<?> kc = null;
                    for (TreeNode<K,V> p = r;;) { //遍歷查找新結點存放位置
                        int dir, ph;
                        K pk = p.key;
                        if ((ph = p.hash) > h)
                            dir = -1;
                        else if (ph < h)
                            dir = 1;
                        //key有實現Comparable接口則使用compareTo()進行比較,否則採用tieBreakOrder中默認的比較方式,即比較hashCode。
                        else if ((kc == null &&
                                  (kc = comparableClassFor(k)) == null) ||
                                 (dir = compareComparables(kc, k, pk)) == 0)
                            dir = tieBreakOrder(k, pk);
                            TreeNode<K,V> xp = p;
                        if ((p = (dir <= 0) ? p.left : p.right) == null) {  //左子節點或右子節點爲空則在p下添加新結點,否則p的值更新爲子節點繼續查找。紅黑樹中結點p.left <= p <= p.right
                            x.parent = xp;  //保存新結點的父結點
                            if (dir <= 0)
                                xp.left = x; //排序小的放左邊
                            else
                                xp.right = x;  //排序大的放右邊
                            r = balanceInsertion(r, x);  //平衡紅黑樹
                            break;
...
            this.root = r;
...
        }

putTreeVal()與此方法遍歷方式類似不再介紹。

擴容實現

寫這篇文章主要就是想講講擴容,Let’s go!

什麼時候會擴容?

  1. 使用put()添加元素時會調用addCount(),內部檢查sizeCtl看是否需要擴容。
  2. tryPresize()被調用,此方法被調用有兩個調用點:
  • 鏈表轉紅黑樹(put()時檢查)時如果table容量小於64(MIN_TREEIFY_CAPACITY),則會觸發擴容。
  • 調用putAll()之類一次性加入大量元素,會觸發擴容。

addCount()

addCount()與tryPresize()實現很相似,我們先以addCount()分析下擴容邏輯:

	private final void addCount(long x, int check) {
        ...
        //check就是結點數量,有新元素加入成功才檢查是否要擴容。
        if (check >= 0) {
            Node<K,V>[] tab, nt; int n, sc;
            //s表示加入新元素後容量大小,計算已省略。
            //新容量大於當前擴容閾值並且小於最大擴容值才擴容,如果tab=null說明正在初始化,死循環等待初始化完成。
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (n = tab.length) < MAXIMUM_CAPACITY) {
                int rs = resizeStamp(n);  //@1
                //sc<0表示已經有線程在進行擴容工作
                if (sc < 0) {
                    //條件1:檢查是對容量n的擴容,保證sizeCtl與n是一塊修改好的
                    //條件2與條件3:應該是進行sc的最小值或最大值判斷。
                    //條件4與條件5: 確保tranfer()中的nextTable相關初始化邏輯已走完。
                    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))  //有新線程參與擴容則sizeCtl加1
                        transfer(tab, nt);
                }
                //沒有線程在進行擴容,將sizeCtl的值改爲(rs << RESIZE_STAMP_SHIFT) + 2),原因見下面sizeCtl值的計算分析。
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
                s = sumCount();
            }
        }
    }

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 bit shift for recording size stamp in sizeCtl.
 */
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;

static final int resizeStamp(int n) {
        return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}

Integer.numberOfLeadingZeros(n)用於計算n轉換成二進制後前面有幾個0。這個有什麼作用呢?
首先ConcurrentHashMap的容量必定是2的冪次方,所以不同的容量n前面0的個數必然不同,這樣可以保證是在原容量爲n的情況下進行擴容。
(1 << (RESIZE_STAMP_BITS - 1)即是1<<15,表示爲二進制即是高16位爲0,低16位爲1:

0000 0000 0000 0000 1000 0000 0000 0000

所以resizeStamp()的返回值(rs) 高16位置0,第16位爲1,低15位存放當前容量n,用於表示是對n的擴容。
rs與RESIZE_STAMP_SHIFT配合可以求出新的sizeCtl的值,分情況如下:

  • sc < 0
    已經有線程在擴容,將sizeCtl+1並調用transfer()讓當前線程參與擴容。
  • sc >= 0
    表示沒有線程在擴容,使用CAS將sizeCtl的值改爲(rs << RESIZE_STAMP_SHIFT) + 2)。

rs即resizeStamp(n),記temp=rs << RESIZE_STAMP_SHIFT。如當前容量爲8時rs的值:

//rs
0000 0000 0000 0000 1000 0000 0000 1000
//temp = rs << RESIZE_STAMP_SHIFT,即 temp = rs << 16,左移16後temp最高位爲1,所以temp成了一個負數。
1000 0000 0000 1000 0000 0000 0000 0000
//sc = (rs << RESIZE_STAMP_SHIFT) + 2)
1000 0000 0000 1000 0000 0000 0000 0010

那麼在擴容時sizeCtl值的意義便如下圖所示:

高15位 低16位
容量n 並行擴容線程數+1

tryPresize()

private final void tryPresize(int size) {
        //根據傳入的size計算出真正的新容量,新容量需要是2的冪次方。
        int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
            tableSizeFor(size + (size >>> 1) + 1);
        int sc;
        while ((sc = sizeCtl) >= 0) {
            Node<K,V>[] tab = table; int n;
            if (tab == null || (n = tab.length) == 0) {
                n = (sc > c) ? sc : c;   //table未初始化則給一個初始容量
                //後面相似代碼不再講解
                if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                    try {
                        if (table == tab) {
                            @SuppressWarnings("unchecked")
                            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                            table = nt;
                            sc = n - (n >>> 2);
                        }
                    } finally {
                        sizeCtl = sc;
                    }
                }
            }
            else if (c <= sc || n >= MAXIMUM_CAPACITY)
                break;
            else if (tab == table) {
                int rs = resizeStamp(n);
                if (sc < 0) {
                    Node<K,V>[] nt;
                    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);
            }
        }
    }

transfer()

jdk1.8版本的ConcurrentHashMap支持併發擴容,上面已經分析了一小部分,下面這個方法是真正進行並行擴容的地方。

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)  //每個線程處理桶的最小數目,可以看出核數越高步長越小,最小16個。
            stride = MIN_TRANSFER_STRIDE; // subdivide range
        if (nextTab == null) {
            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;  //擴容保護
                return;
            }
            nextTable = nextTab;
            transferIndex = n;  //擴容總進度,>=transferIndex的桶都已分配出去。
        }
        int nextn = nextTab.length;
          //擴容時的特殊節點,標明此節點正在進行遷移,擴容期間的元素查找要調用其find()方法在nextTable中查找元素。
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab); 
        //當前線程是否需要繼續尋找下一個可處理的節點
        boolean advance = true;
        boolean finishing = false; //所有桶是否都已遷移完成。
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            //此循環的作用是確定當前線程要遷移的桶的範圍或通過更新i的值確定當前範圍內下一個要處理的節點。
            while (advance) {
                int nextIndex, nextBound;
                if (--i >= bound || finishing)  //每次循環都檢查結束條件
                    advance = false;
                //遷移總進度<=0,表示所有桶都已遷移完成。
                else if ((nextIndex = transferIndex) <= 0) {  
                    i = -1;
                    advance = false;
                }
                else if (U.compareAndSwapInt
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {  //transferIndex減去已分配出去的桶。
                    //確定當前線程每次分配的待遷移桶的範圍爲[bound, nextIndex)
                    bound = nextBound;
                    i = nextIndex - 1;
                    advance = false;
                }
            }
            //當前線程自己的活已經做完或所有線程的活都已做完,第二與第三個條件應該是下面讓"i = n"後,再次進入循環時要做的邊界檢查。
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                if (finishing) {  //所有線程已幹完活,最後才走這裏。
                    nextTable = null;
                    table = nextTab;  //替換新table
                    sizeCtl = (n << 1) - (n >>> 1); //調sizeCtl爲新容量0.75倍。
                    return;
                }
                //當前線程已結束擴容,sizeCtl-1表示參與擴容線程數-1。
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
	                //還記得addCount()處給sizeCtl賦的初值嗎?相等時說明沒有線程在參與擴容了,置finishing=advance=true,爲保險讓i=n再檢查一次。
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)   
                        return;
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);  //如果i處是ForwardingNode表示第i個桶已經有線程在負責遷移了。
            else if ((fh = f.hash) == MOVED)
                advance = true; // already processed
            else {
                synchronized (f) {  //桶內元素遷移需要加鎖。
                    if (tabAt(tab, i) == f) {
                        Node<K,V> ln, hn;
                        if (fh >= 0) {  //>=0表示是鏈表結點
                            //由於n是2的冪次方(所有二進制位中只有一個1),如n=16(0001 0000),第4位爲1,那麼hash&n後的值第4位只能爲0或1。所以可以根據hash&n的結果將所有結點分爲兩部分。
                            int runBit = fh & n;
                            Node<K,V> lastRun = f;
                            //找出最後一段完整的fh&n不變的鏈表,這樣最後這一段鏈表就不用重新創建新結點了。
                            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;
                            }
                            //lastRun之前的結點因爲fh&n不確定,所以全部需要重新遷移。
                            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);
                            }
                            //低位鏈表放在i處
                            setTabAt(nextTab, i, ln);
                            //高位鏈表放在i+n處
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);  //在原table中設置ForwardingNode節點以提示該桶擴容完成。
                            advance = true;
                        }
                        else if (f instanceof TreeBin) { //紅黑樹處理。
                            ...

helpTransfer()

添加、刪除節點之處都會檢測到table的第i個桶是ForwardingNode的話會調用helpTransfer()方法。

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

併發擴容總結

  1. 單線程新建nextTable,新容量一般爲原table容量的兩倍。
  2. 每個線程想增/刪元素時,如果訪問的桶是ForwardingNode節點,則表明當前正處於擴容狀態,協助一起擴容完成後再完成相應的數據更改操作。
  3. 擴容時將原table的所有桶倒序分配,每個線程每次最小分配16個桶,防止資源競爭導致的效率下降。單個桶內元素的遷移是加鎖的,但桶範圍處理分配可以多線程,在沒有遷移完成所有桶之前每個線程需要重複獲取遷移桶範圍,直至所有桶遷移完成。
  4. 一箇舊桶內的數據遷移完成但不是所有桶都遷移完成時,查詢數據委託給ForwardingNode結點查詢nextTable完成(這個後面看find()分析)。
  5. 遷移過程中sizeCtl用於記錄參與擴容線程的數量,全部遷移完成後sizeCtl更新爲新table容量的0.75倍。

擴容節結束!其它常用操作再說下。

get()

public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        int h = spread(key.hashCode());
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            if ((eh = e.hash) == h) {  //總是檢查頭結點
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            else if (eh < 0)  //在遷移或都是TreeBin
                return (p = e.find(h, key)) != null ? p.val : null;
            while ((e = e.next) != null) {  //鏈表則直接遍歷查詢
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

可以看到get()操作如果查詢鏈表不用加鎖,如果有紅黑樹結構的話e.find()方法內部實現需要獲取鎖。

remove()

	public V remove(Object key) {
        return replaceNode(key, null, null);
    }

    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;
            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) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            validated = true;
                            for (Node<K,V> e = f, pred = null;;) {
                                K ek;
                                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))) {  //cv不爲null則替換,否則是刪除。
                                        oldVal = ev;
                                        if (value != null)
                                            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) {
                            //...
                        }
                    }
                }
                if (validated) {
                    if (oldVal != null) {
                        if (value == null)
                            addCount(-1L, -1);
                        return oldVal;
                    }
                    break;
                }
            }
        }
        return null;
    }

主要改進

CAS無鎖算法與synchronized保證併發安全,支持併發擴容,數據結構變更爲數組+鏈表+紅黑樹,提高性能。

jdk1.8版棄用變量

Segment

只有序列化時會用到。

loadFactor

僅用於構造函數中設定初始容量,已不能影響擴容閾值,jdk1.8版本中閾值計算基本恆定爲0.75。

concurrencyLevel

只能影響初始容量,table容量大小與它無關。

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