ConcurrentHashMap -1.8 源码解析
加锁机制
在JDK1.7之前,ConcurrentHashMap是通过分段锁机制来实现的,所以其最大并发度受Segment的个数限制。因此,在JDK1.8中,ConcurrentHashMap的实现原理摒弃了这种设计,而是选择了与HashMap类似的数组+链表+红黑树的方式实现,而加锁则采用CAS和synchronized实现。
存储结构
Java8 的 ConcurrentHashMap 相对于 Java7 来说变化比较大,从1.7的 Segment 数组 + HashEntry 数组 + 链表变成了1.8的Node 数组 + 链表 / 红黑树。当冲突链表达到一定长度时,链表会转换成红黑树。
重要参数
sizeCtl
它的值决定着当前的初始化状态。
- -1 说明正在初始化
- -N 说明有N-1个线程正在进行扩容
- 表示 table 初始化大小,如果 table 没有初始化
- 表示 table 容量,如果 table已经初始化。
/*
该字段控制table(也被称作hash桶数组)的初始化和扩容。
sizeCtl为负数的时候,表示table初始化或者扩容。
sizeCtl = -1 表示已经初始化。
sizeCtl = -(1+正在扩容的线程数)
*/
private transient volatile int sizeCtl;
static final int MOVED = -1; // 表示正在转移(扩容)
static final int TREEBIN = -2; // 表示已经转换成树
static final int RESERVED = -3; // 表示正在
//最大容量(table最大容量是2的30次方)
private static final int MAXIMUM_CAPACITY = 1 << 30;
//默认容量(table默认初始化容量16。扩容总是2的n次方。)
private static final int DEFAULT_CAPACITY = 16;
//默认并发级别
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
//默认的负载因子0.75(当前已使用容量 >= 负载因子*总容量的时候,进行resize扩容)
private static final float LOAD_FACTOR = 0.75f;
//转红黑树阈值,当桶内链表长度>=8时,会将链表转成红黑树
static final int TREEIFY_THRESHOLD = 8;
//红黑树还原链表阈值,当桶内node小于6时,红黑树会转成链表
static final int UNTREEIFY_THRESHOLD = 6;
//最小树型化容量(table的总容量,要大于64,桶内链表才转换为树形结构,否则当桶内链表长度>=8时会扩容)
static final int MIN_TREEIFY_CAPACITY = 64;
构造函数
/**
使用默认的初始表大小 (16) 创建一个新的空映射。
*/
public ConcurrentHashMap() {
}
/**
构造函数,其初始表大小可容纳指定数量的元素,而无需动态调整大小。
@param initialCapacity 初始容量。如果元素的初始容量为负,则抛出异常
@throws IllegalArgumentException
*/
public ConcurrentHashMap(int initialCapacity) {
//如果初始容量为负数抛出异常
if (initialCapacity < 0)
throw new IllegalArgumentException();
//如果初始容量>=最大容量逻辑右移一位就赋值最大容量
// 否则返回大于输入参数且最近的2的整数次幂的数
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
//赋值给sizeCtl参数
this.sizeCtl = cap;
}
initTable()初始化
使用 sizeCtl 中记录的大小初始化表
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
// 如果table为空或者长度为0,进入while准备开始初始化。
while ((tab = table) == null || tab.length == 0) {
// 将sizeCtl赋值给sc。如果sizeCtl<0说明有线程正在初始化,当前线程要进入等待状态
if ((sc = sizeCtl) < 0)
// 线程进入等待
Thread.yield(); // lost initialization race; just spin
// 将sizeCtl设置为-1,代表抢到了锁,开始进行初始化操作
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
//再次判断表是否为空
if ((tab = table) == null || tab.length == 0) {
//判断sc实际为(sizeCtl),构造函数时代表了初始化容量
//如果有指定初始化容量,就用用户指定的,否则用默认的16.
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
// 生成一个长度为n(上面的容量)的Node数组
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
//将地址赋给table
table = tab = nt;
// 重新设置sizeCtl=数组长度 - (数组长度 >>>2)
// 如果 n 为 16 的话,那么这里 sc = 12
// 其实就是 0.75 * 长度(默认的扩容阈值)
sc = n - (n >>> 2);
}
} finally {
// 重新设置sizeCtl
sizeCtl = sc;
}
break;
}
}
return tab;
}
put操作
/**
将指定的键映射到此表中的指定值。键和值都不能为空。
可以通过使用与原始键相同的键调用 {get 方法来检索该值。
@param key 与指定值关联的键
@param value 与指定键关联的值
@return 与 key 关联的前一个值,如果 key没有映射,则为 null
@throws NullPointerException 如果指定的键或值为空
*/
public V put(K key, V value) {
return putVal(key, value, false);
}
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());
// 用来记录所在table数组中的桶的中链表的个数,后面会用于判断是否链表过长需要转红黑树
int binCount = 0;
//for循环用break跳出
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 如果数组"空",进行数组初始化
if (tab == null || (n = tab.length) == 0)
// 初始化table
tab = initTable();
//i为下标,用(数组长度-1)&hash值计算得出
//调用tabAt()获取数组中该下标对应的元素
//如果这位置为空
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 使用CAS 操作将这个新值(将新值放入结点,再将结点放入期中)即可
// 如果 CAS 失败,那就是有并发操作,继续循环
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
// 如果头结点hash值为-1,则为ForwardingNode结点,说明正在扩容
else if ((fh = f.hash) == MOVED)
// 帮助数据迁移,这个等到看完数据迁移部分的介绍后,再理解这个就很简单了
tab = helpTransfer(tab, f);
else { // 到这里就是说,f 是该位置的头结点,而且不为空
V oldVal = null;
// 获取数组该位置的头结点的监视器锁,锁住头结点
synchronized (f) {
//双重检测锁,检测加锁前是否被修改
if (tabAt(tab, i) == f) {
if (fh >= 0) { // 头结点的 hash 值大于 0,说明是链表
// 用于累加,记录链表的长度
binCount = 1;
// 遍历链表
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 如果发现了"相等"的 key,判断是否要进行值覆盖,然后跳出循环
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
// 没发现相等的key,到了链表的最末端,将这个新值放到链表的最后面
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;
}
}
}
}
//如果链表的长度不为0
if (binCount != 0) {
// 判断是否要将链表转换为红黑树,临界值和 HashMap 一样,也是 8
if (binCount >= TREEIFY_THRESHOLD)
// 这个方法和 HashMap 中稍微有一点点不同,那就是它不是一定会进行红黑树转换,
// 如果当前数组的长度小于 64,那么会选择进行数组扩容,而不是转换为红黑树
treeifyBin(tab, i);
if (oldVal != null)
//返回旧值
return oldVal;
break;
}
}
}
// 计数器加1,完成新增后,table扩容,就是这里面触发
addCount(1L, binCount);
//新增后返回空
return null;
}
扩容
两个方法调用扩容
1.每次添加完后,调用的addCount中有调用transfer扩容
2.桶中链表大于8调用treeifyBin方法转红黑树的方法的时候,在该方法中会判断table当前总容量是否大于64,如果table当前总容量小于64,不会转红黑树,而是调用tryPresize方法尝试扩容,tryPresize方法中会调用transfer扩容
扩容怎么保证线程安全
1.多个线程都做扩容的时候,由字段transferIndex表示当前已分配的桶到什么下标了,对transferIndex字段的修改是用的CAS,每个线程先获取自己处理哪个区间的桶,每个线程自己迁移自己的桶,互不打扰。一个线程最少处理16个桶。
比如,现在数组长度为32,线程A迁移0-15的桶,线程B迁移16-31的桶。当前哪些区间的桶被分配的的临界值是transferIndex表示,对它的修改是CAS的,所以多线程扩容线程安全
2.如果有线程去写concurrenthashmap,发现现在正在扩容,则去帮组扩容。如果有线程去读,发现正在扩容,则通过桶上的forwdingNode去新的map中去读。
源码
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
// stride 在单核下直接等于 n,多核模式下为 (n>>>3)/NCPU,最小值是 16(每个做扩容的线程至少处理16个桶)
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
//如果nextTab为空,新建一个是原来2倍长度的nextab
if (nextTab == null) {
try {
// 容量翻倍
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 是 ConcurrentHashMap 中的属性
nextTable = nextTab;
// transferIndex 也是 ConcurrentHashMap 的属性,用于控制迁移的位置
transferIndex = n;
}
int nextn = nextTab.length;
// ForwardingNode 翻译过来就是正在被迁移的 Node
// 这个构造方法会生成一个Node,key、value 和 next 都为 null,关键是 hash 为 MOVED
// 后面我们会看到,原数组中位置 i 处的节点完成迁移工作后,
// 就会将位置 i 处设置为这个 ForwardingNode,用来告诉其他线程该位置已经处理过了
// 所以它其实相当于是一个标志。
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// advance 指的是做完了一个位置的迁移工作,可以准备做下一个位置的了
boolean advance = true;
//所有桶是否都已迁移完成
boolean finishing = false; // to ensure sweep before committing nextTab
/*
* 下面这个 for 循环,最难理解的在前面,而要看懂它们,应该先看懂后面的,然后再倒回来看
*
*/
// i 是位置索引,bound 是边界,注意是从后往前
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
//这个while是给当前线程分配迁移任务,即它负责迁移哪几个桶,它要处理的桶的下标范围
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
//用CAS设置transfer减去已分配的桶,并发扩容保证线程安全,每个扩容的线程根据这个字段扩容自己分配到区间的桶,各不干扰
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
// /确定当前线程每次分配的待迁移桶的范围为[bound, nextIndex)
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) {
// 所有的迁移操作已经完成
nextTable = null;
// 将新的 nextTab 赋值给 table 属性,完成迁移
table = nextTab;
// 重新计算 sizeCtl: n 是原数组长度,所以 sizeCtl 得出的值将是新数组长度的 0.75 倍
sizeCtl = (n << 1) - (n >>> 1);
return;
}
// 之前我们说过,sizeCtl 在迁移前会设置为 (rs << RESIZE_STAMP_SHIFT) + 2
//当前线程已结束扩容,sizeCtl-1表示参与扩容线程数-1
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
//确定当前线程每次分配的待迁移桶的范围为[bound, nextIndex)
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
// 到这里,说明 (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT,
// 也就是说,所有的迁移任务都做完了,也就会进入到上面的 if(finishing){} 分支了
finishing = advance = true;
i = n; // recheck before commit
}
}
// 如果位置 i 处是空的,没有任何节点,那么放入刚刚初始化的 ForwardingNode ”空节点“
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
// 该位置处是一个 ForwardingNode,代表该位置已经迁移过了
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
// 对数组该位置处的结点加锁,开始处理数组该位置处的迁移工作
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
// 头结点的 hash 大于 0,说明是链表的 Node 节点
if (fh >= 0) {
// 下面这一块和 Java7 中的 ConcurrentHashMap 迁移是差不多的,
// 需要将链表一分为二,
// 找到原链表中的 lastRun,然后 lastRun 及其之后的节点是一起进行迁移的
// lastRun 之前的节点需要进行克隆,然后分到两个链表中
int runBit = fh & n;
Node<K,V> lastRun = f;
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;
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);
// 将原数组该位置处设置为ForwardingNode,代表该位置已经处理完毕,
// 其他线程一旦看到该位置的 hash 值为 MOVED,就不会进行迁移了
setTabAt(tab, i, fwd);
// advance 设置为 true,代表该位置已经迁移完毕
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;
}
}
// 如果一分为二后,节点数少于 8,那么将红黑树转换回链表
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;
// 将 ln 放置在新数组的位置 i
setTabAt(nextTab, i, ln);
// 将 hn 放置在新数组的位置 i+n
setTabAt(nextTab, i + n, hn);
// 将原数组该位置处设置为 fwd,代表该位置已经处理完毕,
// 其他线程一旦看到该位置的 hash 值为 MOVED,就不会进行迁移了
setTabAt(tab, i, fwd);
// advance 设置为 true,代表该位置已经迁移完毕
advance = true;
}
}
}
treeifyBin()链表转红黑树
private final void treeifyBin(Node<K,V>[] tab, int index) {
// b表示需要转换为红黑树的那个桶在数组中的下标
Node<K,V> b; int n, sc;
// 如果table不为空
if (tab != null) {
// 如果table长小于64,调用tryPresize扩容,而不是转换为红黑树
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
// 调用tryPresize扩容
tryPresize(n << 1);
// 开始进行转换为红黑树
// 得到要转换为红黑树的链表的头节点,如果头节点不为空,并且头节点的hash >= 0
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
// 锁住头节点
synchronized (b) {
// 双重锁检查,以防在锁之前又被其他线程改变了该桶头节点的内容
if (tabAt(tab, index) == b) {
// hd表示红黑树的根节点
// tl表示preNode
TreeNode<K,V> hd = null, tl = null;
// 遍历链表
for (Node<K,V> e = b; e != null; e = e.next) {
// 把链表中的每个Node包装为TreeNode
TreeNode<K,V> p =
new TreeNode<K,V>(e.hash, e.key, e.val,
null, null);
if ((p.prev = tl) == null)
// 确定红黑树的根节点
hd = p;
else
// 还是要维护next指针
tl.next = p;
tl = p;
}
//用TreeBin<K,V>包装红黑树的根节点,并放入到数组的桶中
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
get操作
**get操作是无锁的。**即使TreeBin的find函数有可能会加TreeBin的内部读锁,但也是非阻塞的。
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// 得到key的哈希值
int h = spread(key.hashCode());
// 如果tabele不为空,并且tab.length大于0,得到桶的头节点不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 桶的头节点的哈希值等于要get的key的哈希值
if ((eh = e.hash) == h) {
//桶的头节点的key等于要get的key
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
//那么桶的头节点就是我们要get的节点,直接返回头节点的value
return e.val;
}
// 桶的头节点的哈希值小于0,表示在红黑树上或者正在扩容
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
// 这里表示在桶的链表上
// 遍历该桶的链表找到get的节点,返回节点的value
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
这里可以看到get方法是没有加锁的。Node中的value和nextNode定义的时候用了volatile来保证可见性和有序性
ConcurrentHashMap的同步机制
核心:
-
读读不互斥
-
读写不互斥
-
写写互斥
5.1 读读不互斥
可以看到整个get方法是没有锁的,无论是synchronized所还是JUC包中的那些Lock,都没有。
读方法细分:
-
桶内只是链表,直接遍历链表读了。无任何锁性质的东西。
-
桶内有红黑树,在TreeBin的find方法中操作,是读读同时进行的时候,用红黑树查找。这里用CAS设置LockState字段,不要去理解成成读读互斥了,并不是一个线程读完了才能让另一个线程读,是只有把lockState字段增加这个操作本身互斥而已。
举个例子:
两个线程同时读,A线程用CAS设置了LockState字段为读后,A开始真的做读操作。B线程并不需要等A读完才能读,B线程只需要等A设置完LockState字段后,自己就能去设置LockState字段了然后开始读了。
5.2 读写不互斥
虽然写方法put会用synchronized去锁桶内头节点/红黑树的根节点,
但是:读方法get没有任何锁性质的东西,不需要获取桶内头节点的synchronized锁
读方法细分:
1.桶内只是链表,直接遍历链表读了。无任何锁性质的东西。
2.桶内有红黑树,在TreeBin的find方法中操作,是读写同时进行的时候,用链表方式查找。
5.3 写写互斥
写和写并发的时候肯定是互斥的,一个线程在写的时候用synchronized对桶内头节点/红黑树的根节点加锁,另一个线程要写同一个桶,首先要用synchronized获取锁,此时只有等待,等正在写的线程写完后释放锁,再去竞争资源。
ConCurrentHashMap在1.7和1.8区别
ConCurrentHashMap 1.8 相比 1.7的话,主要改变为:
- 去除
Segment + HashEntry + Unsafe
的实现,改为Synchronized + CAS + Node + Unsafe
的实现,其实 Node 和 HashEntry 的内容一样,但是HashEntry是一个内部类。 - 用 Synchronized + CAS 代替 Segment ,这样锁的粒度更小了,并且不是每次都要加锁了,CAS尝试失败了在加锁。
- put()方法中 初始化数组大小时,1.8不用加锁,因为用了个
sizeCtl
变量,将这个变量置为**-1**,就表明table正在初始化。
下面简单介绍下主要的几个方法的一些区别:
1. put() 方法
JDK1.7中的实现:
ConCurrentHashMap 和 HashMap 的put()方法实现基本类似,所以主要讲一下为了实现并发性,ConCurrentHashMap 1.7 有了什么改变
- 需要定位 2 次 (segments[i],segment中的table[i])
- 先通过key的
rehash值的高位
和segments数组大小-1
相与得到在 segments中的位置 - 然后在通过
key的rehash值
和table数组大小-1
相与得到在table中的位置
- 先通过key的
- 没获取到 segment锁的线程,不能进行put操作,不是像HashTable一样去挂起等待,而是会去做一下put操作前的准备:
- 计算table[i]的位置(你的值要put到哪个桶中)
- 通过首节点first遍历链表找有没有相同key
- 在进行1、2的期间最多自旋64次获取锁,超过则线程挂起
JDK1.8中的实现:
-
- 先拿到根据
rehash值
定位,拿到table[i]的首节点first
,然后:- 如果为
null
,通过CAS
的方式把 value put进去 - 如果
非null
,并且first.hash == -1
,说明其他线程在扩容,参与一起扩容 - 如果
非null
,并且first.hash != -1
,Synchronized锁住 first节点,判断是链表还是红黑树,遍历插入。
- 如果为
- 先拿到根据
2. get() 方法
JDK1.7中的实现:
- 由于变量
value
是由volatile
修饰的,java内存模型中的happen before
规则保证了 对于 volatile 修饰的变量始终是 写操作 先于 读操作 的,并且还有 volatile 的 内存可见性 保证修改完的数据可以马上更新到主存中,所以能保证在并发情况下,读出来的数据是最新的数据。 - 如果get()到的是
null
值才去加锁。
JDK1.8中的实现:
- 和 JDK1.7类似
3. resize() 方法
JDK1.7中的实现:
- 跟HashMap的 resize() 没太大区别,都是在 put() 元素时去做的扩容,所以在1.7中的实现是获得了锁之后,在单线程中去做扩容(1.new个2倍数组 2.遍历old数组节点搬去新数组)。
JDK1.8中的实现:
- 支持并发扩容,HashMap扩容在1.8中由头插改为尾插(为了避免死循环问题),ConcurrentHashmap也是,迁移也是从尾部开始,扩容前在桶的头部放置一个hash值为-1的节点(ForwardingNode),这样别的线程访问时就能判断是否该桶已经被其他线程处理过了。
4. 计算size
JDK1.7中的实现:
- 先采用不加锁的方式,计算两次,如果两次结果一样,说明是正确的,返回。
- 如果两次结果不一样,则把所有 segment 锁住,重新计算所有 segment的
Count
的和
JDK1.8中的实现:
由于没有segment的概念,所以只需要用一个 baseCount
变量来记录当前节点的个数
- 先尝试通过CAS 修改
baseCount
- 如果多线程竞争激烈,某些线程CAS失败,那就CAS尝试将
CELLSBUSY
置1,成功则可以把baseCount变化的次数
暂存到一个数组counterCells
里,后续数组counterCells
的值会加到baseCount
中。 - 如果
CELLSBUSY
置1失败又会反复进行CASbaseCount
和 CAScounterCells
数组