ConcurrentHashMap 胡思乱想

629 阅读16分钟

ConcurrentHashMap胡思乱想

参考文献

ConcurrentHashMap是1.8提供的一个线程安全的HashMap,其特点是:

  • 有效利用自旋+CAS+synchronized的方式为不同粒度的并发操作提供同步保障
  • 使用多线程协作的方式对扩充这个耗时操作提供高效支持

正是上述两个优化使得相较于前面的版本,有了很大的效率提升。

putVal

对于该方法流程如下:

  • 如果table为null,或者table.length为0,则执行initTable初始化;
  • 如果对应下标的桶为null,则执行插入,注意,执行插入时采用CAS操作,在并发情况下,可能CAS插入失败,因此,CAS需要搭配自旋使用,所以可以理解putVal方法内部是一个很大的for循环;
  • 如果对应下标的同正在执行迁移,则参与到迁移工作中来
  • 否则,在对应下标的桶上执行尾部插入,而该行为使用针对桶的头结点施加synchronized关键字的方式实现
  • 执行addCount修改记录的桶的个数

到此,产生问题:Q为何插入新的桶节点时采用CAS+自旋的方式,而往桶中进行尾插入时就是用synchronized的方式?

首先,在插入新的桶节点时,由于此时下标index指示的桶节点为null,无法使用synchronized关键字进行加锁;如果想要使用synchronized关键字,则只能定义一个全局锁Lock对该全局锁进行加锁,那么这种方式就会扩大加锁范围,造成对不同位置插入桶的操作之间造成并发问题,违反了设计初衷;
那么,对桶进行尾部节点插入时,为啥不能使用CAS+自旋的方式呢?不可以!!这样会造成错误结果,比如当前桶的链表如下:1->2->3->4->5,而两个线程并发删除3和4,那么进行CAS时进行分别进行如下两个操作CAS(2.next, 3, 3.next),CAS(3.next, 4, 4.next)很显然,由于操控的是不同的节点,这两个CAS操作不会发生冲突,可以并行执行,假如第一个CAS先执行完,那么桶变为了1->2->4->5,此时第二个CAS执行完后桶的状态依然是1->2->4->5。原因在于,虽然不同节点的CAS操作本身没有问题,但是他们之间可能产生依赖性,就像上面的实例所示,只有第二个CAS先于第一个CAS执行是正确的结果;而解决代码之间的依赖性,synchronized就顺理成章的用上了

方法如下:

final V putVal(K key, V value, boolean onlyIfAbsent) {
    // 不允许插入空值或空键
    // 允许value空值会导致get方法返回null时有两种情况:
    // 1. 找不到对应的key2. 找到了但是value为null;
    // 当get方法返回null时无法判断是哪种情况,在并发环境下containsKey方法已不再可靠,
    // 需要返回null来表示查询不到数据。允许key空值需要额外的逻辑处理,占用了数组空间,且并没有多大的实用价值。
    // HashMap支持键和值为null,但基于以上原因,ConcurrentHashMap是不支持空键值。
    if (key == null || value == null) throw new NullPointerException();
    // 高低位异或扰动hashcode,和HashMap类似
    // 但有一点点不同,后面会讲,这里可以简单认为一样的就可以
    int hash = spread(key.hashCode());
    // bincount表示链表的节点数
    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();
        // 情况二:目标下标对象为null
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 重点:采用CAS进行插入
            if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))
                break;
        }
        // 情况三:数组正在扩容,帮忙迁移数据到新的数组
        // 同时会新数组,下次循环就是插入到新的数组
        // 关于扩容的内容后面再讲,这里理解为正在扩容即可
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        // 情况四:直接对节点进行加锁,插入数据
        // 下面代码很多,但逻辑和HashMap插入数据大同小异
        // 因为已经上锁,不涉及并发安全设计
        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;
                        }
                    }
                    else if (f instanceof ReservationNode)
                        throw new IllegalStateException("Recursive update");
                }
            }
            // 判断是否需要转化为红黑树,和返回旧数值
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    // 总数+1;这是一个非常硬核的设计
    // 这是ConcurrentHashMap设计中的一个重点,后面我们详细说
    addCount(1L, binCount);
    return null;
}

// 这个方法和HashMap
static final int spread(int h) {
    return (h ^ (h >>> 16)) & HASH_BITS;
}

initTable

该方法负责对table进行初始化,而该方法也很清晰明了,就是采用CAS+自旋的方式竞争是对table进行初始化,还是等待;而CAS修改的只是一个int类型的标志,能够成功修改为特定的某个值则说明获取到初始化权限,而其他线程只能让出时间片等待table初始化完成。因此,该方法就是一个大的while循环,循环体针为一个针对CAS结果的if分支:

  • 成功CAS则进行table初始化;
  • 否则执行Thread.yield让出时间片。

这里对CAS失败的线程为何要让出时间片,而不是return呢?原因为:initTable方法发生在第一次putVal时,在并发场景下,紧接着的、并发的putVal方法都需要依赖于initTable创建出的table数组,因此,CAS竞争失败的线程也不能够直接退出,需要等待table的初始化完成。这又是一个依赖性的问题!!

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    // 这里的循环是采用自旋的方式而不是上锁来初始化
    // 首先会判断数组是否为null或长度为0
    // 没有在构造函数中进行初始化,主要是涉及到懒加载的问题
    while ((tab = table) == null || tab.length == 0) {
        // sizeCtl是一个非常关键的变量;
        // 默认为0,-1表示正在初始化,<-1表示有多少个线程正在帮助扩容,>0表示阈值
        if ((sc = sizeCtl) < 0)
            Thread.yield(); // 让出cpu执行时间

        // 通过CAS设置sc为-1,表示获得自选锁
        // 其他线程则无法进入初始化,进行自选等待
        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>>>2表示1/4*n,也就相当于0.75n
                    sc = n - (n >>> 2);
                }
            } finally {
                // 把sc赋值给sizeCtl
                sizeCtl = sc;
            }
            break;
        }
    }
    // 最后返回tab数组
    return tab;
}

transfer

该方法负责进行桶节点的迁移,transfer方法实现中大体是两个循环嵌套:

  • for循环负责控制筒下标i,每次将i位置的同进行迁移
  • while循环其实是一个CAS自旋操作,其作用有两个:当前线程第一次执行transfer方法时使用CAS搭配while循环来设置线程负责迁移的范围,也即初始化i和bound两个索引变量;之后while循环负责移动i来实现对负责范围内所有筒的遍历

同样,该方法中修改各个状态等操作、将空节点设置为ForwardingNode等操作都是使用CAS自旋,而迁移整个筒采用的是synchronized对筒节点加锁的方式。这也提一下,对于加了锁的同步代码内部,不需要CAS了

同样,每一次for循环中除了经历while循环维护筒下标i之外,还要经历如下处理过程

  1. 根据i和bound判断本次任务是否完成,如果完成则需要CAS修改ConcurrentHashMap的全局状态记录sizeCtl;
  2. 如果下标i位置的筒为null,那么直接将其标记为ForwardingNode;
  3. 如果下标i位置的筒为ForwardingNode,那么advance设置为true,则在下一次for循环中会开启while循环来将i向前移动一个位置;
  4. 否则,对下标i位置的筒进行迁移操作;这个操作会对筒的头结点使用synchronized关键字加锁,迁移操作同HashMap一样;迁移完毕后会将下标i位置的筒节点设置为ForwardingNode;并将advance设置为true,则在下一次for循环中会开启while循环来将i向前移动一个位置。

正是transfer方法将HashMap中最耗时的迁移操作使用多线程来完成,使得并发访问ConcurrentHashMap中的成员的时候,遇到transfer时不是阻塞住,而是helpTransfer,这种多线程共同完成同一个任务的方案,大大提高了ConcurrentHashMap的效率

// 这里的两个参数:tab表示旧数组,nextTab表示新数组
// 创建新数组的线程nextTab==null,其他的线程nextTab等于第一个线程创建的数组
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;
    // stride表示每次前进的步幅,最低是16
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range

    // 如果新的数组还未创建,则创建新数组
    // 只有一个线程能进行创建数组
    if (nextTab == null) {            
        try {
            @SuppressWarnings("unchecked")
            // 扩展为原数组的两倍
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            nextTab = nt;
        } catch (Throwable ex) {      
            // 扩容失败出现OOM,直接把阈值改成最大值
            sizeCtl = Integer.MAX_VALUE;
            return;
        }
        // 更改concurrentHashMap的内部变量nextTable
        nextTable = nextTab;
        // 迁移的起始值为数组长度
        transferIndex = n;
    }

    int nextn = nextTab.length;
    // 标志节点,每个迁移完成的数组下标都会设置为这个节点
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    // advance表示当前线程是否要前进
    // finish表示迁移是否结束
    // 官方的注释表示在赋值为true之前,必须再重新扫描一次确保迁移完成,后面会讲到
    boolean advance = true;
    boolean finishing = false; // to ensure sweep before committing nextTab

    // i表示当前线程迁移数据的下标,bound表示下限,从后往前迁移
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;

        // 这个循环主要是判断是否需要前进,如果需要则CAS更改下个bound和i
        while (advance) {
            int nextIndex, nextBound;
            // 如果还未到达下限或者已经结束了,advance=false
            if (--i >= bound || finishing)
                advance = false;
            // 每一轮循环更新transferIndex的下标
            // 如果下一个下标是0,表示已经无需继续前进
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;
            }
            // 利用CAS更改bound和i继续前进迁移数据
            else if (U.compareAndSwapInt
                     (this, TRANSFERINDEX, nextIndex,
                      nextBound = (nextIndex > stride ?
                                   nextIndex - stride : 0))) {
                bound = nextBound;
                i = nextIndex - 1;
                advance = false;
            }
        }

        // i已经达到边界,说明当前线程的任务已经完成,无需继续前进
        // 如果是第一个线程需要更新table引用
        // 协助的线程需要将sizeCtl减一再退出
        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
            // 如果已经更新完成,则更新table引用
            if (finishing) {
                nextTable = null;
                table = nextTab;
                // 同时更新sizeCtl为阈值
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }
            // 线程完成自己的迁移任务,将sizeCtl减一
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                // 这里sc-2不等于校验码,说明此线程不是最后一个线程,还有其他线程正在扩容
                // 那么就直接返回,他任务已经完成了
                // 最后一个线程需要重新把整个数组再扫描一次,看看有没有遗留的
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return;
                // finish设置为true表示已经完成
                // 这里把i设置为n,重新把整个数组扫描一次
                finishing = advance = true;
                i = n; // recheck before commit
            }
        }
        // 如果当前节点为null,表示迁移完成,设置为标志节点
        else if ((f = tabAt(tab, i)) == null)
            // 这里的设置有可能会失败,所以不能直接设置advance为true,需要再循环
            advance = casTabAt(tab, i, null, fwd);
        // 当前节点是ForwardingNode,表示迁移完成,继续前进
        else if ((fh = f.hash) == MOVED)
            advance = true; // already processed
        else {
            // 给头节点加锁,进行迁移
            // 加锁后下面的内容就不涉及并发控制细节了,就是纯粹的数据迁移
            // 思路和HashMap差不多,但也有一些不同,多了一个lastRun
            // 读者可以阅读一下下面源码,这部分比较容易理解
            synchronized (f) {
                // 上锁之后再判断一次看该节点是否还是原来那个节点
                // 如果不是则重新循环
                if (tabAt(tab, i) == f) {
                    Node<K,V> ln, hn;
                    // hash值大于等于0表示该节点是普通链表节点
                    if (fh >= 0) {
                        int runBit = fh & n;
                        Node<K,V> lastRun = f;
                        // ConcurrentHashMap并不是直接把整个链表分为两个
                        // 而是先把尾部迁移到相同位置的一段先拿出来
                        // 例如该节点迁移后的位置可能为 1或5 ,而链表的情况是:
                        // 1 -> 5 -> 1 -> 5 -> 5 -> 5
                        // 那么concurrentHashMap会先把最后的三个5拿出来,lastRun指针指向倒数第三个5
                        for (Node<K,V> p = f.next; p != null; p = p.next) {
                            int b = p.hash & n;
                            if (b != runBit) {
                                runBit = b;
                                lastRun = p;
                            }
                        }
                        // 判断尾部整体迁移到哪个位置
                        if (runBit == 0) {
                            ln = lastRun;
                            hn = null;
                        }
                        else {
                            hn = lastRun;
                            ln = null;
                        }
                        for (Node<K,V> p = f; p != lastRun; p = p.next) {
                            int ph = p.hash; K pk = p.key; V pv = p.val;
                            // 这个node节点是改造过的
                            // 相当于使用头插法插入到链表中
                            // 这里的头插法不须担心链表环,因为已经加锁了
                            if ((ph & n) == 0)
                                ln = new Node<K,V>(ph, pk, pv, ln);
                            else
                                hn = new Node<K,V>(ph, pk, pv, hn);
                        }
                        // 链表构造完成,把链表赋值给数组
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                        // 设置标志对象,表示迁移完成
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }
                    // 树节点的处理,和链表思路相同,不过他没有lastRun,直接分为两个链表,采用尾插法
                    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;
                    }
                }
            }
        }
    }
}

addCount

在HashMap中还需要维护成员的个数,而在并发场景下,该值的维护也是使用多线程维护的,核心方法是addCount方法。addCount方法也不是一上来就进行复杂的操作来维护size,在addCount方法中首先会进行一些简单的尝试,然后再调用fullAddCount方法保证count能够被正确维护;在addCount方法中进行的尝试为:

  1. 尝试使用一次CAS直接修改baseCount值;
  2. 根据hash生成的index索引获取counterCells中对应位置的对象,尝试使用一次CAS修改该对象的值。

当上面两个方法都没有成功,或者CounterCells为null时,会调用fullAddCount方法保证count能够被正确维护。

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方法:
        // 1. 数组为null且直接修改basecount失败
        // 2. hash后的数组下标CounterCell对象为null
        // 3. CAS修改CounterCell对象失败
        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;

        }

        // 如果长度<=1不需要扩容(说实话我觉得这里有点奇怪)
        if (check <= 1)
            return;
        s = sumCount();
    }
    if (check >= 0) {
        // 扩容相关逻辑,下面再讲
    }
}

fullAddCount方法内部使用了复杂的机制来维护CounterCells数组以及baseCount。而fullAddCount方法很长,修改CounterCell本身是一件很容易的事情,因此,采用CAS+自旋的方式完成,因此,该方法整体上就是一个大的for循环,里面根据情况进行操作,发生冲突后就进入下一个循环,直到成功:

  1. 如果CounterCells数组为null,那么就获取唯一的独占锁,并初始化数组;而在当前数组还没初始化完成的情况下,同putVal一样,其他的任何操作依赖于这个CounterCells数组,因此,并发失败的线程也不会闲着,当然也没有使用Thread.yield让出时间片,而是尝试使用CAS直接修改baseCount,如果真的修改成功了,就直接return了;
  2. 如果CounterCells数组不为null
    1. 如果当前线程获取下标索引后指示的CounterCell对象是null,那么就尝试获取唯一独占锁,成功后创建CounterCell对象;
    2. 否则,尝试通过CAS修改下表索引指示的CounterCell对象的值;
    3. 都不成功,则需要对CounterCells数组进行扩容。

值得注意的是,在fullAddCount方法中的全局唯一的独占锁,其实就是一个int标志,并发环境下通过CAS对该变量进行操作,操作失败则伴随着for自旋再次竞争。

有个疑问:addCount这种机制虽然也是为不同的线程维护CounterCell来共同维护count,但是这种复杂的方式是否过于追求并发带来的那一点效率的提升?直接大家都使用CAS自旋来修改baseCount不行吗?

private final void fullAddCount(long x, boolean wasUncontended) {
    int h;
    // 如果当前线程随机数为0,强制初始化一个线程随机数
    // 这个随机数的作用就类似于hashcode,不过他不需要被查找
    // 下面每次循环都重新获取一个随机数,不会让线程都堵在同一个地方
    if ((h = ThreadLocalRandom.getProbe()) == 0) {
        ThreadLocalRandom.localInit();      
        h = ThreadLocalRandom.getProbe();
        // wasUncontended表示没有竞争
        // 如果为false表示之前CAS修改CounterCell失败,需要重新获取线程随机数
        wasUncontended = true;
    }

    // 直译为碰撞,如果他为true,则表示需要进行扩容
    boolean collide = false;      

    // 下面分为三种大的情况:
    // 1. 数组不为null,对应的子情况为CAS更新CounterCell失败或者countCell对象为null
    // 2. 数组为null,表示之前CAS更新baseCount失败,需要初始化数组
    // 3. 第二步获取不到锁,再次尝试CAS更新baseCount
    for (;;) {
        CounterCell[] as; CounterCell a; int n; long v;

        // 第一种情况:数组不为null
        if ((as = counterCells) != null && (n = as.length) > 0) {
            // 对应下标的CounterCell为null的情况
            if ((a = as[(n - 1) & h]) == null) {
                // 判断当前锁是否被占用
                // cellsBusy是一个自旋锁,0表示没被占用
                if (cellsBusy == 0) {    
                    // 创建CounterCell对象
                    CounterCell r = new CounterCell(x); 
                    // 尝试获取锁来添加一个新的CounterCell对象
                    if (cellsBusy == 0 &&
                        U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                        boolean created = false;
                        try {               
                            CounterCell[] rs; int m, j;
                            // recheck一次是否为null
                            if ((rs = counterCells) != null &&
                                (m = rs.length) > 0 &&
                                rs[j = (m - 1) & h] == null) {
                                rs[j] = r;
                                // created=true表示创建成功
                                created = true;
                            }
                        } finally {
                            // 释放锁
                            cellsBusy = 0;
                        }
                        // 创建成功也就是+1成功,直接返回
                        if (created)
                            break;
                        // 拿到锁后发现已经有别的线程插入数据了
                        // 继续循环,重来一次
                        continue;          
                    }
                }
                // 到达这里说明想创建一个对象,但是锁被占用
                collide = false;
            }
            // 之前直接CAS改变CounterCell失败,重新获取线程随机数,再循环一次
            else if (!wasUncontended)       // CAS already known to fail
                wasUncontended = true;      // Continue after rehash
            // 尝试对CounterCell进行CAS
            else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
                break;
            // 如果发生过扩容或者长度已经达到虚拟机最大可以核心数,直接认为无碰撞
            // 因为已经无法再扩容了
            // 所以并发线程数的理论最高值就是NCPU
            else if (counterCells != as || n >= NCPU)
                collide = false;            // At max size or stale
            // 如果上面都是false,说明发生了冲突,需要进行扩容
            else if (!collide)
                collide = true;
            // 获取自旋锁,并进行扩容
            else if (cellsBusy == 0 &&
                     U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                try {
                    if (counterCells == as) {// Expand table unless stale
                        // 扩大数组为原来的2倍
                        CounterCell[] rs = new CounterCell[n << 1];
                        for (int i = 0; i < n; ++i)
                            rs[i] = as[i];
                        counterCells = rs;
                    }
                } finally {
                    // 释放锁
                    cellsBusy = 0;
                }
                collide = false;
                // 继续循环
                continue;                   
            }

            // 这一步是重新hash,找下一个CounterCell对象
            // 上面每一步失败都会来到这里获取一个新的随机数
            h = ThreadLocalRandom.advanceProbe(h);
        }

        // 第二种情况:数组为null,尝试获取锁来初始化数组
        else if (cellsBusy == 0 && counterCells == as &&
                 U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
            boolean init = false;
            try {
                // recheck判断数组是否为null
                if (counterCells == as) {
                    // 初始化数组
                    CounterCell[] rs = new CounterCell[2];
                    rs[h & 1] = new CounterCell(x);
                    counterCells = rs;
                    init = true;
                }
            } finally {
                // 释放锁
                cellsBusy = 0;
            }
            // 如果初始化完成,直接跳出循环,
            // 因为初始化过程中也包括了新建CounterCell对象
            if (init)
                break;
        }

        // 第三种情况:数组为null,但是拿不到锁,意味着别的线程在新建数组,尝试直接更新baseCount
        else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
            // 更新成功直接返回
            break;                         
    }
}