在之前的博文《 ConCurrentHashMap 的源码分析》,系统分析了源码。
文章特别长,本篇将计数拿出来单讲
计数
所谓的计数,指的是 ConCurrentHashMap
存了多少个 键值对。
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
比如调用 size 方法,返回键值对的个数。这里是把 long 型,强转为 int。
当然还有更为精确的方法,mappingCount
public long mappingCount() {
long n = sumCount();
return (n < 0L) ? 0L : n; // ignore transient negative values
}
这两个方法,都是调用了 sumCount()
方法。
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
long sum = baseCount;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
代码很清晰,比较容易理解,计数来自两部分,一个是 baseCount
,
另一个是 各个 CounterCell
的和。
/**
* Base counter value, used mainly when there is no contention,
* but also as a fallback during table initialization
* races. Updated via CAS.
*/
private transient volatile long baseCount;
/**
* Table of counter cells. When non-null, size is a power of 2.
*/
private transient volatile CounterCell[] counterCells;
/**
* A padded cell for distributing counts. Adapted from LongAdder
* and Striped64. See their internal docs for explanation.
*/
@sun.misc.Contended static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}
源码中注释写的很清楚,counterCells 大小是 2 的 n 次方。
CounterCell 这个内部类中的 成员变量只有一个,且是用 vllatile 修饰的。
@sun.misc.Contended 这个可以解决**伪共享
**的问题,本文不再展开讲。
以上代码很容易理解,计数方法也没有要讲的。
这里,重点说说,counterCells 是如何初始化的,它的工作原理
以及在多线程环境下,是如何计数的。
重点说明:
先明确一点,计数时,要么修改了 baseCount
,要么 修改了 CounterCell
对象中 value
的值。
在 put()
方法 中调用了 addCount(1L, binCount);
这个方法。
另外在删除元素 remove
方法时,也调用了 addCount(-1L, -1);
这个方法。
addCount(
) 这个方法其中一个重要功能,就是计数。
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;
}
if (check <= 1)
return;
s = sumCount();
}
if (check >= 0) {
// 可能触发扩容,前面讲过了,跟计数无关系,这里省略
}
}
假设 counterCells
还没有初始化,现在有 4 个线程,同时执行为个方法,
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x))
(as = counterCells) != null
,按假设来说,这个返回 false,看下一个判断。
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)
四个线程同时执行这行,那只有一个线程会执行成功,
修改 **baseCount
**的值,不进入方法体。
其它三个线程执行方法体中的方法。
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;
}
按假设来讲,as == null
返回 true,三个线程都会执行 fullAddCount(x, uncontended);
。
这个方法就是进行精确的计数的。等会再细讲。
现在假设 counterCells
已经初始化,且 size 大于0。
还是 4 个线程同时执行 addCount
方法。
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x))
(as = counterCells) != null
,按假设来说,这个返回 true,
后半个判断就不会执行了,四个线程都进入方法体。
四个线程都会执行 (a = as[ThreadLocalRandom.getProbe() & m]) == null
ThreadLocalRandom.getProbe()
这个方法是返回一个随机数,彼此之间不同。
as[ThreadLocalRandom.getProbe() & m]
这个是得到数组中的一个元素。
求下标的公式和 HashMap
的一样,这里也不展开来说。
每个线程获取的随机数是不一样的,各自算出一个下标。
假设极端情况下,4 个线程计算出的下标是同一个。
若该下标处元素为 null,那 4 个线程,都会执行 fullAddCount(x, uncontended);
。
若该下标处元素不为null,那 4 个线程都执行
!(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
那有一个线程成功修改 counterCell 中 value 的值,完成这个线程的计数,返回。
剩下的3个线程,都会执行 fullAddCount(x, uncontended);
。
总结下:
执行 addCount 时计数时,
若 counterCells
这个数组未初始化, 非竞争条件下,修改 baseCount
,否则执行 fullAddCount(x, uncontended);
若 counterCells
这个数组已经初始化, 非竞争条件下,修改 对应的 counterCell
,否则执行 fullAddCount(x, uncontended);
如果上面的都清楚了,咱就开始分析 fullAddCount(x, uncontended);
方法
private final void fullAddCount(long x, boolean wasUncontended) {
int h;
if ((h = ThreadLocalRandom.getProbe()) == 0) {
ThreadLocalRandom.localInit(); // force initialization
h = ThreadLocalRandom.getProbe();
wasUncontended = true;
}
boolean collide = false; // True if last slot nonempty
for (;;) {
CounterCell[] as; CounterCell a; int n; long v;
if ((as = counterCells) != null && (n = as.length) > 0) {
if ((a = as[(n - 1) & h]) == null) {
if (cellsBusy == 0) { // Try to attach new Cell
CounterCell r = new CounterCell(x); // Optimistic create
if (cellsBusy == 0 &&
U.compareAndSwapInt(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
wasUncontended = true; // Continue after rehash
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
break;
else if (counterCells != as || n >= NCPU)
collide = false; // At max size or stale
else if (!collide)
collide = true;
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
try {
if (counterCells == as) {// Expand table unless stale
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; // Retry with expanded table
}
h = ThreadLocalRandom.advanceProbe(h);
}
else if (cellsBusy == 0 && counterCells == as &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean init = false;
try { // Initialize table
if (counterCells == as) {
CounterCell[] rs = new CounterCell[2];
rs[h & 1] = new CounterCell(x);
counterCells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break; // Fall back on using base
}
}
代码看起来很长,但分解开来,没有特别难的。
if ((h = ThreadLocalRandom.getProbe()) == 0) {
ThreadLocalRandom.localInit(); // force initialization
h = ThreadLocalRandom.getProbe();
wasUncontended = true;
}
这段是为了让线程获取随机数,先让其初始化。
具体细节就不展开讲了,有兴趣的,可以看下 ThreadLocalRandom
源码。
for (;;) {
CounterCell[] as; CounterCell a; int n; long v;
if ((as = counterCells) != null && (n = as.length) > 0) {
// 修改某个CounterCell,有可能会对 counterCells 进行扩容
}
else if (cellsBusy == 0 && counterCells == as &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
// 初始化 counterCells
}
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break; // Fall back on using base
// 修改 baseCount 的值
}
这整体是一个无限循环,三个分支执行成功一个,就可以跳出循环。
否则一直执行。最终的结果是 要么修改了 baseCount
,要么 修改了 CounterCell
先看第二个分支,初始化 counterCells
CounterCell[] as; CounterCell a; int n; long v;
if ((as = counterCells) != null && (n = as.length) > 0){
……
}
else if (cellsBusy == 0 && counterCells == as &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean init = false;
try { // Initialize table
if (counterCells == as) {
CounterCell[] rs = new CounterCell[2];
rs[h & 1] = new CounterCell(x);
counterCells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
cellsBusy == 0 && counterCells == as
这两个判断是并发的控制。
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)
这个是CAS修改 cellsBusy
,
cellsBusy
是 1 指在初始化或是在操作 CounterCells
。
执行成功的那个线程,初始化 counterCells
/**
* Spinlock (locked via CAS) used when resizing and/or creating CounterCells.
*/
private transient volatile int cellsBusy;
然后看方法体
boolean init = false;
try { // Initialize table
if (counterCells == as) {
CounterCell[] rs = new CounterCell[2];
rs[h & 1] = new CounterCell(x);
counterCells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
counterCell
的初始容量是2,并且把 new 一个 CounterCell
对象,记录了 数值
初始化完毕后,将 cellsBusy
设置为 0;
再看第一个分支
CounterCell[] as; CounterCell a; int n; long v;
if ((as = counterCells) != null && (n = as.length) > 0) {
if ((a = as[(n - 1) & h]) == null) {
if (cellsBusy == 0) { // 当前没有线程操作数组
CounterCell r = new CounterCell(x); // new 一个对象出来
if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) { // CAS 设置 cellsBusy 为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; // 操作完后,一定将 cellsBusy 改回 0
}
if (created)
break; // 设置到成功的情况下,退出。
continue; // Slot is now non-empty
}
}
collide = false;
}
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
break; // 对应下标处不为空,且CAS 记录数据成功,退出
else if (counterCells != as || n >= NCPU) // 当数组size 大于CPU 数量,不让数组扩容。
collide = false; // At max size or stale
else if (!collide)
collide = true;
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
try { // 执行扩容
if (counterCells == as) {// Expand table unless stale
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; // Retry with expanded table
}
h = ThreadLocalRandom.advanceProbe(h);
}
if ((a = as[(n - 1) & h]) == null)
意思是 某下标处元素为空,这个小分支,
总的逻辑就是,线程安全情况下, new 一个 CounterCell
对象,记录了数值,设置到数组中。
这个应该好理解,我把注释写代码里了。
第二个大分支就是想办法设置 CounterCell
,必要情况下扩容数组。
第三个大分支更好理解,修改 baseCount
。
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break;
fullAddCount
方法 代码虽然很长,但各分支逻辑很清晰。
就是:在充分控制并发的情况下,修改 CounterCell
或是 baseCount
。
修改不成功,就重试,直到成功为止。
总结
计数总逻辑,通过 CounterCell
或是 baseCount
,来保证多线程环境下计数问题。
- 无竞争条件下,执行
put()
方法时,操作baseCount
实现计数 - 首次竞争条件下,执行
put()
方法,会初始化CounterCell
,并实现计数 CounterCell
一旦初始化,计数就优先使用CounterCell
- 每个线程,要么修改
CounterCell
、要么修改baseCount
,实现计数 CounterCell
在竞争特别严重时,会扩容。(扩容上限与 CPU 核数有关,不会一直扩容)