ConcurrentHashMap 源码目前在网络上已有众多解析。本文章主要关注其数量 size 的相关并发实现,试图自己解析该实现,如有错漏,请指正(预警:本文较啰嗦)。
ConcurrentHashMap#size 的关注点在于并发下结构性变更导致的数目统计,是如何实现高性能、高准确性的。实际上,ConcurrentHashMap 目前使用的算法与 LongAddr 相同,均是分段技术。代码上不能说一模一样,只能说是直接复制粘贴。
1.相关概念
1.1 在无并发的情况下,使用单一的属性 baseCount 进行累计(CAS),一旦操作不成功,进入并发场景。
1.2 在并发情况下,使用多个分区,将增加或减少的个数累计到相应的分区,可很大程度避免多线程操作同一对象的并发问题,最后在获取时,将各分区的值进行相加,得到一个弱一致性的数量值。
1.3 CounterCells 是一个数组,线程需要与其 length 进行 & 操作,以 hash 到具体的 CounterCell 中,所以为线程增加了属性 probe,该属性由位操作实现,得到一个随机的值。同时该值可以被更新。
CounterCell counterCell = cs[ThreadLocalRandom.getProbe() & m])
1.4 除非显式调用其他 API,否则 Thread 的 probe 的初值是 0。显式 API 有:
java.util.concurrent.ThreadLocalRandom#current
public static ThreadLocalRandom current() {
if (U.getInt(Thread.currentThread(), PROBE) == 0)
localInit();
return instance;
}
java.util.concurrent.ThreadLocalRandom#localInit
static final void localInit() {
int p = probeGenerator.addAndGet(PROBE_INCREMENT);
int probe = (p == 0) ? 1 : p; // skip 0
long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
Thread t = Thread.currentThread();
U.putLong(t, SEED, seed);
U.putInt(t, PROBE, probe);
}
1.5 线程随机数 probe 的重新赋值,是以 2 的倍数操作,与 ConcurrentHashMap 的扩容、大小等 2 的倍数设定匹配
2. addCount 解析
2.1 数量变更的入口
入口是 addCount,方法签名如下:
private final void addCount(long x, int check)
外部引用的方法有:
主要是结构性变更的操作,其中:
- 当增加时,binCount > 0;
- 当减少时,binCount < 0;
- 当 binCount = 0时,实际上不调用 addCount,因为数量不变(存在于方法 replaceNode 等方法中)。
通过代码分析,binCount 作为 addCount 中 check 的实际传参,在外部方法中是对结构性变更导致影响个数的记录。在新增操作中,
- 对普通节点,即 Node 的链表节点,操作如果需要遍历,则 check = 遍历个数;
对树结构,check 总是等于 2。
2.2 方法实现
进入方法后,addCount 实现分为两部分:
- 对数量的并发处理
- 因外部变更性行为导致的数量变动,进一步检查变动后是否需要对 ConcurrentHashMap 进行扩容操作
这里就需要关注:并发处理是如何实现的;扩容检查的触发机制是如何的。
2.2.1 数量并发处理
private final void addCount(long x, int check) {
// s -> size ; b -> baseCount
CounterCell[] cs; long b, s;
// 如果 counterCells 不为空,则表示有多线程使用过了 cell,
// 一旦用过 counterCells 则后续都要用它来计数
// 如果没有用过,说明未遇到过并发,继续使用最 baseCount 增加个数,失败则转入多线程处理方式
if ((cs = counterCells) != null ||
!U.compareAndSetLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell c; long v; int m;
// uncontended : 不受欢迎的、不建议的,这里表示没有竞争(预想没有)
// 标记是因为 CounterCells 为空才进入 fullAddCount 还是因为 CAS 并发替换失败才进入 fullAddCount
// uncontended = true 即表示由无并发进入,否则为 CAS 并发失败进入
boolean uncontended = true;
if (cs == null || (m = cs.length - 1) < 0 ||
(c = cs[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSetLong(c, CELLVALUE, v = c.value, v + x))) {
// 只有此方法为 counterCells 赋值
// 只有 CAS 替换失败时,uncontended 才为 false,否则都为 true
// CAS 替换这里,因为 ThreadLocalRandom#getProbe 的原因,还存在以下情况
// 1. Thread 的 Probe 默认情况下是不会初始化的,所以只有初值 0 ,只有通过 ThreadLocalRandom#current 才会强制初始化
// 也就是说,多线程情况下,如果线程都没有初始化过 Probe,此时如果 CAS 都替换成功,则值都增加到同一个 CounterCell[0] 中,
// 这种情况并不影响并发考虑,因为 CAS 替换的最终也只是一个值,而不是一个队列或数组,不会过分增加负担
// 另外两种情况在方法内被处理
fullAddCount(x, uncontended);
// 并发导致执行 fullAddCount 后,直接退出,不再考虑扩容检查
return;
}
// ... 省略
}
这里的操作步骤也与前面描述类似,按以下次序:CounterCells 是否为空 > CAS(baseCount) > CAS(CounterCells[n]) > fulllAddCount
。
addCount 有一种思想,即当一个集合发生过并发时,其后续发生的并发可能性也越高,所以会先判断 CounterCells 是否为空,如果不为空,则后续不再考虑在 baseCount 上操作。这种思想应当应用在大部分场景中。
addCount 对 CounterCells 的操作不多,实际上,对 CounterCells 操作主要在方法 fulllAddCount 中,而 addCount 负责无并发处理及收集进入 fullAddCount 所需的条件和判断。其中,uncontented 就表示:进入 fullAddCount 的原因,是否因为并发修改同一个 CounterCell。
2.2.2 数量变动引发的扩容检查
ConcurrentHashMap 的并发性支持的一个主要重点就是扩容,而数量变动是一个检查扩容的主要入口。
private final void addCount(long x, int check) {
CounterCell[] cs; long b, s;
if ((cs = counterCells) != null ||
!U.compareAndSetLong(this, BASECOUNT, b = baseCount, s = b + x)) {
// ...省略,前述已有代码
fullAddCount(x, uncontended);
// 并发导致执行 fullAddCount 后,直接退出,不再考虑扩容检查
return;
}
// 仅在无竞争条件下进行检查(只有一个则不需要进行检查)
// 影响数 <= 1,减少扩容的并发检查
if (check <= 1)
return;
// 而此操作则是在无并发的情况下的处理(外部 CAS(CounterCells[n]) 成功)
s = sumCount();
}
if (check >= 0) {
// nt -> nextTable
// n -> num,sc -> sizeCtl
Node<K,V>[] tab, nt; int n, sc;
// 当前存储大于 75%,且总大小小于最大容量
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
// resizeStamp 纯粹只是移位来保证右 16 位为0,可用来控制作为线程最大数
// 左 16 位实际并没有保留太多信息(因为明显:resizeStamp(4)、resizeStamp(5)、resizeStamp(6)、(7) 是相同的结果
int rs = resizeStamp(n) << RESIZE_STAMP_SHIFT;
if (sc < 0) {
// 限制线程的最大或最小,当达到最大 65535(默认) 或 1 条时,则直接跳出
// rs + 1 --> 最少线程数(相当于不正确的情况了,因为起始时最少是 rs + 2)
// rs + MAX_RESIZERS --> 最多线程数
// 或其他情况,则不再辅助转移,如:nextable 已为 null 或 transferIndex <= 0(说明已结束)
// 前两个条件是限制线程数,后两个条件是扩容已经结束
if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
(nt = nextTable) == null || transferIndex <= 0)
break;
if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// 如果 sc > 0,说明是刚开始,因为 sc < 0 时,表示有多少条线程在进行转移是:sc-1
// 所以这里要 rs + 2
else if (U.compareAndSetInt(this, SIZECTL, sc, rs + 2))
transfer(tab, null);
s = sumCount();
}
}
可见,check 是影响扩容检查的主要元素。而前面说过,check 是外部修改影响到的个数 —— 可通过查看方法 putValue 等,check 实际传参为 binCount。
- 对普通节点,即 Node 的链表节点,操作如果需要遍历,则 check = 遍历个数;
- 对树结构,check 总是等于 2。
同时,check 在此方法中,起作用总是在非并发情况下的线程,并发 CAS 失败的将进入 fullAddCount 然后直接退出。
查看方法实现,可见由 check 导致扩容检查的作用范围是:
- 非并发情况,直接 CAS(baseCount);
- 并发但是 CAS(CounterCell[n]) 成功的线程,且操作的 Node 子节点需大于 1 或为树节点,而其他 CAS 失败将不会导致扩容检查。
这也将导致,部分 Node 链表的变更操作(putVal等)不会导致扩容检查——只变更了一个的情况;而树结构,因为总是 2,所以只要其 CAS 成功,则该线程辅助扩容检查。
猜测 check 的逻辑目的,一方面想要尽可能保证扩容检查,一方面又不想太过频繁导致性能影响,所以,
- 当无并发情况,则负载正常,扩容检查压力也较小(扩容的压力应该来自线程,而非内存或其他压力);
- 当有并发情况时,因为是 CAS 操作,只考虑其中一条线程来保证,而非全部线程都执行此逻辑,毕竟并发情况下,负载已高(其导致的扩容情况也更频繁),再让所有线程都执行此逻辑,可能得不偿失。
为什么树形结构总是为 2, 如果它只更新了一个节点呢?
实际上,出现了树形结构,应该是该 Map 有扩容的考虑期望在里面,因为默认都是链表不足则先扩展大小,如果扩展大小后(大于64)依旧有新元素导致树化,即该 Map 具有较多的元素变更操作,可更关注于树形结构变更。这也近似于前面说到的思想,就好像跟偏向锁一样,总是偏向于某一种预定情况,而扩容检查则偏向于并发度高,操作频繁的。
剩下的扩容操作已有其他文章解析,此处不再赘述。
3.fullAddCount 解析
fullAddCount 的功能:负责 CounterCells 的主要操作,即:初始化、更新、扩展、并发控制。
3.1 先看初始化
外部方法 addCount 在 CAS( baseCounter ) 失败后,会进入 fullAddCount 初始化 CounterCells。为了保证安全初始化 CounterCells,这里的做法是:死循环+CAS ( cellsBusy )。cellsBusy=0 为正常状态,cellsBusy=1 表示 CounterCells 正在被修改,通过 CAS 保证并发安全。
// See LongAdder version for explanation
private final void fullAddCount(long x, boolean wasUncontended) {
int h;
if ((h = ThreadLocalRandom.getProbe()) == 0) {
ThreadLocalRandom.localInit(); // force initialization
h = ThreadLocalRandom.getProbe();
wasUncontended = true;
}
// collide : 碰撞,是否冲突(这属于进入方法 rehash 后的重新一次冲突检测)
boolean collide = false; // True if last slot nonempty
for (;;) {
if ((cs = counterCells) != null && (n = cs.length) > 0) {
// ... 省略
}
// cellsBusy 为 0 且 counterCells == cs 相等(cs 在前面被赋值:cs=counterCells),CAS 替换 cellsBusy 成功,进入初始化
else if (cellsBusy == 0 && counterCells == cs &&
U.compareAndSetInt(this, CELLSBUSY, 0, 1)) {
// 是否初始化完成
boolean init = false;
try { // Initialize table
if (counterCells == cs) {
// 默认创建 2 个 Cell
CounterCell[] rs = new CounterCell[2];
// 哈希线程,取其中一个 Cell
rs[h & 1] = new CounterCell(x);
counterCells = rs;
// 初始化完成,通过 break 跳出返回
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
}
}
CounterCells 初始大小默认为 2 个,通过与线程 Probe 进行 & 操作后,hash 到其中一个,并使用外部传入的变更数量 x 来初始化,然后标记完成。注意,这里初始化了 2 个,但只赋值了一个,所以另外一个 CounterCells[n]=null,这将匹配到另外一个分支去执行。最后,通过 finally 块将 cellBusy 赋回 0。
3.2 变更操作、并发控制、扩容
private final void fullAddCount(long x, boolean wasUncontended) {
int h;
if ((h = ThreadLocalRandom.getProbe()) == 0) {
ThreadLocalRandom.localInit(); // force initialization
h = ThreadLocalRandom.getProbe();
wasUncontended = true;
}
// collide : 碰撞,是否冲突(这属于进入方法 rehash 后的重新一次冲突检测)
boolean collide = false; // True if last slot nonempty
for (;;) {
CounterCell[] cs; CounterCell c; int n; long v;// 非空,且 size 大于 0,通过线程的 hash & size ,定位所在的 CounterCell
if ((cs = counterCells) != null && (n = cs.length) > 0) {
// 第一步:解决问题 c = cs[ThreadLocalRandom.getProbe() & m]) == null,即初始化或扩容产生的新 CounterCell
if ((c = cs[(n - 1) & h]) == null) {
if (cellsBusy == 0) { // Try to attach new Cell
// 初始化值,赋值完成后就 break 了
CounterCell r = new CounterCell(x); // Optimistic create
// CAS 替换,相当于获取锁,只有一条线程能够进入
if (cellsBusy == 0 &&
U.compareAndSetInt(this, CELLSBUSY, 0, 1)) {
// 创建完毕
boolean created = false;
try { // Recheck under lock
CounterCell[] rs; int m, j;
if ((rs = counterCells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
created = true;
}
} finally {
// 释放锁
cellsBusy = 0;
}
if (created)
break;
continue; // Slot is now non-empty
}
}
collide = false;
}
else if (!wasUncontended) // CAS already known to fail
// 此处只有 CAS 失败的才会进入,将为 CAS 尝试做一次 rehash 操作,然后重新尝试
wasUncontended = true; // Continue after rehash
else if (U.compareAndSetLong(c, CELLVALUE, v = c.value, v + x)) // 如果已存在值,则进行 CAS 替换,不成功继续死循环
break;
else if (counterCells != cs || n >= NCPU)
// 边界判断,如果没有正在扩容,则判断是否超过大小边界,是则进行 rehash,否则扩容
// 正处于无效状态或 CounterCell 大小已超过 CPU 个数,此时对 Probe 进行重置(翻倍)
collide = false; // At max size or stale
else if (!collide)
collide = true;
else if (cellsBusy == 0 &&
U.compareAndSetInt(this, CELLSBUSY, 0, 1)) {
try {
// 翻倍
if (counterCells == cs) // Expand table unless stale
counterCells = Arrays.copyOf(cs, n << 1);
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
h = ThreadLocalRandom.advanceProbe(h);
}
// cellsBusy 为 0 且 counterCells == cs 相等(cs 在前面被赋值:cs=counterCells),CAS 替换 cellsBusy 成功,进入初始化
else if (cellsBusy == 0 && counterCells == cs &&
U.compareAndSetInt(this, CELLSBUSY, 0, 1)) {
//...省略
}
else if (U.compareAndSetLong(this, BASECOUNT, v = baseCount, v + x))
// 如果上述两个步骤无法进入,认为出现了错误,使用 CAS(BaseCount) 作为备用/保障选项
break; // Fall back on using base
}
}
fullAddCount 对 cs[ThreadLocalRandom.getProbe() & m]) == null 提供了支持,即赋值初始化。这不算并发影响。
fullAddCount 有种思想:对于 CAS 失败的,总是存了善心,给他们改过的机会 —— 进行 rehash。
CAS 失败有以下情况:
外部原因:即 wasUncontended 标志
- 因为线程从未初始化过 Probe,它们的初值都为0,导致都被 hash 到 CounterCells[0] 并 CAS 失败的,给他们的 Probe 重新赋值,这对应入口方法;
- 线程 Probe 已赋值过,但 CAS 失败的,给他们的 Probe 进行 rehash,这对应分支 if ( !wasUncontended );
内部原因:即 collide 标志。
- 如果外部失败进入后,在此方法中又 CAS 失败的,则 collide = true,为内部 CAS 冲突失败,它们也需要被 rehash。
如果内部 CAS 失败,则尝试扩容,扩容的边界是 CounterCells # length < NCPU(CPU核数),因为理想情况下,一个 CPU 核数执行一个线程,而每个线程能够到 hash 到各自的 cell 上,冲突性低。如果超出 NCPU,核数全开,也只能帮你最多 NCPU 个线程,所以并不是扩容越大越好。扩容前还需先判断当前的状态,如果是正在扩容,则采取 rehash,守株待兔即可。
扩容操作比较简单,在 cellBusy 的保护下,对数组进行翻倍,然后重新循环重试。
负责扩容的线程不再进行 rehash,而不负责扩容的线程,则进行 rehash,提升 Probe,其内部实现也是将 Probe 进行翻倍,与 HashMap 的树的两倍扩容、高低位有异曲同工之妙。
方法大体解析完毕,不过还剩下:wasUncontended 标志、collide 标志。
纵观全实现,感觉这两个参数的作用不大,却在每个分支上都有他们的身影。代码实现上,将他们取消并直接实现为线程 advanceProbe,似乎也并无不妥。
wasUncontended 标志、collide 标志 在某种意义上,具有划分、明确上下文的作用,表示线程的 CAS 冲突性是由外部引起还是内部引起,反过来观察 collide,它的注释是: "True if last slot nonempty",只有在所有的 CounterCell 非空下,才能为 true,也就是说,当 CAS 失败但发现 CounterCells#length < NCPU,不认为属于冲突,可以通过扩容来解决,当扩容到最大值了,此时 CAS 失败,才认为产生了冲突。并且在各种分支中,会根据分支来重置表达含义。
另外一个理解思路是:wasUncontended、collide 在某种程度上,具有保护性能的作用。毕竟,如果 CAS 失败立即 advanceProbe 并重新进入 CAS,在一个高并发的场景中不见得能够比延缓操作要好。按照这种理解,可能需要自己重新修改源码然后编译一份做并发测试才能确认。
3.3 逃生门
除了对 CounterCells 的控制,fullAddCount 还考虑到如果 CounterCell 发生了未知情况无法处理时,后备 baseCount 来保证数据正确。
else if (U.compareAndSetLong(this, BASECOUNT, v = baseCount, v + x))
// 如果上述两个步骤无法进入,使用 CAS(BaseCount) 作为备用/保障选项
break;
也就是说,一开始以为 CounterCells 不为空就不再操作 baseCount 是错误的,baseCount 作为后备保证了流程的完整性和正确性。
总结
以上,是对 ConcurrentHashMap 的方法 addCount 和 fullAddCount 阅读后的一些理解,总的来说,对并发这块的解析不多,因为与 LongAddr 接近,主要是理解一些像 trick 的代码,一开始疑窦丛生,后面又自圆其说,如果错漏,感谢指出。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。