Map源码解析之ConcurrentHashMap(JDK1.8)(二)

版权声明:感谢您的阅读,欢迎讨论并指出不足,可自由转载,但请注明出处和作者 https://blog.csdn.net/qq_39470742/article/details/88288107

Map源码解析之HashMap
Map源码解析之HashMap红黑树
Map源码解析之HashMap补充:集合、迭代器、compute、merge、replace
Map源码解析之LinkedHashMap
Map源码解析之TreeMap
Map源码解析之HashTable
Map源码解析之ConcurrentHashMap(JDK1.8)(一)

本篇文章将在 Map源码解析之ConcurrentHashMap(JDK1.8)(一)的基础上继续解析ConcurrentHashMap.

一、扩容的调用方法

1. ConcurrentHashMap#helpTransfer(Node<K,V>[], Node<K,V>)

该方法的目的是帮助在扩容中的数组实现扩容。在满足条件的情况下,将本线程加入扩容。

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

2. ConcurrentHashMap#tryPresize(int)

根据参数确定是否要扩容以及确定扩容后的新数组容量。有可能是不扩容,有可能触发扩容,有可能在正在扩容的情况下将本线程加入扩容。在批量加入ConcurrentHashMap#putAll(Map)和树化ConcurrentHashMap#treeifyBin方法中被调用。

private final void tryPresize(int size) {
    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;
            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);
        }
    }
}

3. ConcurrentHashMap#addCount(long, int)

在节点数量发生变化时(putVal, replaceNode, compute, merge)调用该发放,第二个参数小于0或者小于2且存在竞争时,不需要考虑扩容。计算处理采用LongAdder。

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;
        }
        //小于2且存在竞争,不处理扩容
        if (check <= 1)
            return;
        s = sumCount();
    }
    //check >= 0时才需要考虑扩容
    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();
        }
    }
}

二、resizeStamp

ConcurrentHashMap可以进行多线程扩容,提高了扩容的效率,但代码的复杂性也大幅增加。而ConcurrentHashMap的扩容算法以resizeStamp(扩容生成戳)作为标记,与resizeStamp密切相关。接下来,专门针对resizeStamp相关的方法进行解析。

1. 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;

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

Integer.numberOfLeadingZeros(n)是返回n用二进制表示时的前面的0的位数,该方法只在扩容相关的方法里调用,且n必为2的次幂,加入n=2^x,则结果为31-x。
1 << (RESIZE_STAMP_BITS - 1),RESIZE_STAMP_BITS 默认为16,最小为6,因此结果默认为1<<16,最小为1<<5。
31-x二进制表示时最多只需要5位数,而1 << (RESIZE_STAMP_BITS - 1)的末尾5位数都是0,因此或运算时两者互不干扰,不同的n计算出来的结果必然不一致。

2. sizeCtl和扩容线程数的关系

根据n计算出来的resizeStamp是需要存储在sizeCtl中的,但是由于sizeCtl为正数时表示下一次扩容的容量,因此需要将resizeStamp转化为负数,具体的转化方式是resizeStamp<<RESIZE_STAMP_SHIFT。因此resizeStamp的第RESIZE_STAMP_BITS位为1,再次向左位移32-RESIZE_STAMP_BITS可以使其第32位即符号位为1,表示负数。
这一点,可以再tryPresize和addCount这两个触发扩容的方法里得到验证。

else if (U.compareAndSwapInt(this, SIZECTL, sc,
                           (rs << RESIZE_STAMP_SHIFT) + 2))
  transfer(tab, null);

在tryPresize、addCount、helpTransfer中每当有新线程加入扩容时,都会将sizeCtl的值加1。

if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);

在transfer中每当有一个线程结束扩容时,都会将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; // recheck before commit
 }

因此,可以通过(sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT判断整个扩容是否结束,是否可以执行最后的变量赋值。
可以得出,sizeCtl为负数时,表示ConcurrentHashMap正在扩容。但此时sizeCtl的值与扩容线程数n的关系并不是
sizeCtl = -(n + 1),而是sizeCtl = (基数 + n)。而且这个基数与容量相对应:(rs << (32 - RESIZE_STAMP_BITS)) + 1。

3. 扩容的resizeStamp条件

接下来看下面一段同样在tryPresize、addCount、helpTransfer方法中都出现的一条if语句,当满足if语句时,都会跳出循环并结束整个方法,不会加入扩容。

if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
      sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
       transferIndex <= 0)
       break;

最后两个判断条件(nt = nextTable) == null || transferIndex <= 0容易理解代表整扩容任务彻底完成,下面重点针对前面三个和reszieStamp相关的判断条件进行展开。

当ConcurrentHashMap正在扩容时,sizeCtl(sc)<0必须成立.
假设此时的容量为2 ^ x,n为参与扩容的线程数,b = RESIZE_STAMP_BITS来简化描写,6 <= b <= 32。
即sc = rs << (32 - b) + 1 + n = ((31 - x) | (1 << (b - 1)) << (32 - b) + 1 + n < -1必须满足
(1)sc == rs + 1
当b = 32时
sc = rs << 0 + 1 = rs + 1,判断条件成立,不能进入扩容,也就是说虽然RESIZE_STAMP_BITS可以允许为32,但当其为32时,只能有一个线程对ConcurrentHashMap进行扩容。
(2)sc == rs + MAX_RESIZERS
当 6 <= b < 32时

31 - x < 31 = 2 ^ 5 - 1,在二进制中最多用5位数表示。
1 << (b - 1)在二进制中只有最高位为1,后续位数全部为0,且后续的位数(b - 1) >= 5,最少5位。
因此(31 - x) | (1 << (b - 1)) = 31 - x + 1 << (b - 1)
又有 n <= MAX_RESIZERS  = 1 << (32 - b) - 1
sc = ((31 - x) | (1 << (b - 1))) << (32 - b) + n + 1 
   <= (31 - x + 1 << (b - 1)) << (32 - b) + 1 << (32 - b) - 1 + 1
   = (32 - x + 1 << (b - 1)) << (32 - b) 
   = (32 - x) << (32 - b) + 1 << 31
32 - x < 32 = 1 << 5, 32 - b <= 32 - 6 = 26
1 << 31 = Integer.MIN
(32 - x) << (32 - b) 必然小于 1 << 5 << 26 = 1 << 31
所以(32 - x) << (32 - b)必然为正,且在Integer的范围内
所以只要满足 0 < n <= MAX_RESIZERS,那么sc的值既不会向下溢出,也不可能为正

参与扩容的线程数量达到最大之后,判断条件sc == rs + MAX_RESIZERS成立,新线程不在参与扩容。
(3)sc >>> RESIZE_STAMP_SHIFT
当n = MAX_RESIZERS,条件(2)成立,新线程不在参与扩容。
因此,这里我们只考虑n < MAX_RESIZERS的情况

n + 1 < MAX_RESIZERS + 1  = 1 << (32 - b) 
(n + 1) >>> (32 - b) 
	>= 1 >>> (32 - b) 
	= 0
n >>> (32 - b) 
	< 1 << (32 - b) >>> (32 - b)
	= 1
所以n >>> (32 - b) = 0
sc >>> (32 - b)
	= rs << (32 - b) >>> (32 - b) + (n + 1) >>> (32 - b)
	= rs << (32 - b) >>> (32 - b)
	= rs

所以当sc >>> RESIZE_STAMP_SHIFT) != rs成立时,说明此时的sc或rs的值发生了变化,此时新线程不应加入扩容。这种情况出现的一个场景就是:进入方法时,扩容正在进行中,但进入该判断条件前,旧扩容完成但新的扩容在进行中。这也是sizeCtl为什么不采用注释中的sizeCtl = - (1 + 进行扩容的线程的原因)

4. resizeStamp的好处

sizeCtl = - (1 + 进行扩容的线程的原因)时会发生的问题,此时上述的判断条件去除sizeStamp相关内容,变成了

if (((nt = nextTable) == null ||
       transferIndex <= 0)
       break;

此时倘若出现下述场景,则极有可能出现错误,无法保证扩容的线程安全。因此CAS算法无法处理ABA问题。
图片来源https://blog.csdn.net/u011392897/article/details/60479937
在这里插入图片描述

采用resizeStamp则不同,其生成过程基于容量2^x,因此不同轮次的扩容过程的rs和sc不可能重复。

sc的值域[ (31 - x) << (32 - b) + 1 << 31 + 1, (32 - x) << (32 - b) + 1 << 31 ]
当x变大为x+1 时[ (30 - x) << (32 - b) + 1 << 31 + 1, (31 - x) << (32 - b) + 1 << 31)]
x + 1时的最大值小于x时的最小值,两者没有重合之处,所以此时使用CAS不可能出现ABA问题。

三、红黑树

ConcurrentHashMap的红黑树结果与HashMap相识,下面主要针对不同点做解析。
在ConcurrentHashMap中,不在把红黑树的头结点直接放到数组中,而是新定义一个数据结构TreeBin,并将其作为数组元素,成员变量root指向红黑树的头结点,first指向红黑树的第一个节点,hash值固定为负数-2。
红黑树节点封装成TreeNode,同HashMap记为类似,不过ConcurrentHashMap对TreeNode的所有操作都通过TreeBin来进行。
由于在对红黑树进行写操作时,有可能会对红黑树的结果造成很大变化,因此TreeBin还维护了一个读写锁waiter,主要状态有写(WRITER)、等待写(WAITER)、读(READER)。
(1)重要的一点,红黑树的 读锁状态 和 写锁状态 是互斥的,但是从ConcurrentHashMap角度来说,读写操作实际上可以是不互斥的
(2) 红黑树的 读、写锁状态 是互斥的,指的是以红黑树方式进行的读操作和写操作(只有部分的put/remove需要加写锁)是互斥的
(3) 但是当有线程持有红黑树的 写锁 时,读线程不会以红黑树方式进行读取操作,而是使用简单的链表方式进行读取,此时读操作和写操作可以并发执行
(4)当有线程持有红黑树的 读锁 时,写线程可能会阻塞,不过因为红黑树的查找很快,写线程阻塞的时间很短
另外一点,ConcurrentHashMap的put/remove/replace方法本身就会锁住TreeBin节点,这里不会出现写-写竞争的情况,因此这里的读写锁可以实现得很简单

3.1 TreeBin#find(int, Object)

final Node<K,V> find(int h, Object k) {
    if (k != null) {
        for (Node<K,V> e = first; e != null; ) {
            int s; K ek;
            //锁状态WAITER或者WRITER,链表方式
            if (((s = lockState) & (WAITER|WRITER)) != 0) {
                if (e.hash == h &&
                    ((ek = e.key) == k || (ek != null && k.equals(ek))))
                    return e;
                e = e.next;
            }
            //红黑树方式,加READER锁
            else if (U.compareAndSwapInt(this, LOCKSTATE, s,
                                         s + READER)) {
                TreeNode<K,V> r, p;
                try {
                    p = ((r = root) == null ? null :
                         r.findTreeNode(h, k, null));
                } finally {
                    Thread w;
                    if (U.getAndAddInt(this, LOCKSTATE, -READER) ==
                        (READER|WAITER) && (w = waiter) != null)
                        LockSupport.unpark(w);
                }
                return p;
            }
        }
    }
    return null;
}

2. TreeBin#lockRoot()

在由于节点变化导致红黑树重组时,需要锁住根节点。修改锁状态为WRITER,修改失败说明存在竞争,根据情况修改锁状态。

private final void lockRoot() {
    if (!U.compareAndSwapInt(this, LOCKSTATE, 0, WRITER))
        contendedLock(); // offload to separate method
}
/**
 * Possibly blocks awaiting root lock.
 */
private final void contendedLock() {
    boolean waiting = false;
    for (int s;;) {
        if (((s = lockState) & ~WAITER) == 0) {
            if (U.compareAndSwapInt(this, LOCKSTATE, s, WRITER)) {
                if (waiting)
                    waiter = null;
                return;
            }
        }
        else if ((s & WAITER) == 0) {
            if (U.compareAndSwapInt(this, LOCKSTATE, s, s | WAITER)) {
                waiting = true;
                waiter = Thread.currentThread();
            }
        }
        else if (waiting)
            LockSupport.park(this);
    }
}

3. TreeBin#unlockRoot()

释放根节点的锁。

/**
* Releases write lock for tree restructuring.
 */
private final void unlockRoot() {
    lockState = 0;
}

猜你喜欢

转载自blog.csdn.net/qq_39470742/article/details/88288107
今日推荐