前言
JDK8提供了一个性能更好的长整型原子类:java.util.concurrent.atomic.LongAdder。
在高并发下,它有着比AtomicLong更好的性能,代价是会消耗更多的内存空间。
有了AtomicLong,为啥还要引入LongAdder?
AtomicLong是通过volatile+CAS的方式来保证并发安全的,它只有一个value值,所有的线程都对这一个value值操作,也就意味着这个value是热点数据。随着并发量越来越高,CAS操作失败的概率也会越来越大,会有越来越多的线程自旋重试,干耗CPU资源,降低性能。关于AtomicLong的详细解释,可以看笔者的另一篇文章:《AtomicLong源码导读》。
为了解决高并发量下AtomicLong的性能问题,Doug Lea他老人家写了个LongAdder。需要说明的是,LongAdder无法完全替代AtomicLong,LongAdder的sum()获取的是一个估计值,要想获取某一时刻的准确值,需要加全局锁。
LongAdder是如何优化的?
LongAdder的代码比AtomicLong复杂的多,它主要的思想还是:分而治之。
LongAdder将AtomicLong中唯一的热点数据进行了分散,它用到了Cell[]数组,每个Cell都是一个资源,线程只操作自己Cell的value,大大减少了热点资源的争用,这和ConcurrentHashMap的分段锁是一个思想。
通过分而治之的思想,将单个热点资源分散到Cell[]数组中,线程只操作自己Cell的value,最终汇总结果时,只需将每个Cell的value累加就是LongAdder的最终结果。
需要注意的是,由于汇总结果时,仍然有线程在对Cell的value进行修改,因此sum()返回的结果只是一个估计值,并不准确。如果需要计算准确值,需要加全局锁,因此LongAdder更适合写多读少的场景。
源码导读
UML
LongAdder的结构并不复杂,和AtomicLong的区别是,它继承了
java.util.concurrent.atomic.Striped64类。Striped64采用了分段锁的思想,它主要用来在高并发下高效的处理64位的数据。
Striped64的设计思想是在高并发下,将热点资源的争用尽量做到分散。在没有争用的情况下,直接操作base变量,和AtomicLong的效果一样。存在多线程争用的情况下,则将热点资源分散到Cell[]数组中。
每个Cell实例都是一个资源,内部维护了一个volatile long value,同时提供了cas()方法,允许线程对value进行CAS修改。
属性
LongAdder本身没有属性,全都继承自父类Striped64。
base是一个被volatile修饰的长整型,它的作用有两个:
- 没有资源争用的情况下,直接操作base即可。
Cell[]初始化的时候,将操作回退到base身上。
cells就是用来分散热点资源的,每个Cell实例本身就可以看作是一个AtomicLong,LongAdder会将Thread的threadLocalRandomProbe属性视为线程的哈希值,然后通过hash & (cells.length-1)计算下标,每个线程只操作自己对应下标的Cell实例,减少热点资源的争用。
cellsBusy可以看做是Striped64内部的一个锁标志,0代表无锁,1代表有锁,通过CAS的方式去修改它。主要是在Cell[] cells创建、扩容时会加锁。
/**
* Cell数组,减少热点资源用的。
*/
transient volatile Cell[] cells;
/**
* 1.没有资源争用的情况下,直接操作base即可。
* 2.cells初始化的时候,将操作回退到base身上。
*/
transient volatile long base;
/**
* 锁标志,0代表无锁,1代表有锁
*/
transient volatile int cellsBusy;
核心方法
add
调用add()方法时,LongAdder首先会判断Cell[]是否已创建,如果没有创建,说明不存在资源争用,则直接调用casBase()去修改base变量即可,如果CAS失败说明存在资源争用,则要去创建Cell[],将热点资源分散开,避免过多的自旋。
如果Cell[]创建了说明已经存在资源争用了,则线程去操作属于自己的Cell即可。
/**
* 添加给定的值x
*/
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
// 如果cells没创建,则直接CAS尝试直接修改base变量
if ((as = cells) != null || !casBase(b = base, b + x)) {
// 存在资源争用的情况
boolean uncontended = true;
// cells没创建
if (as == null || (m = as.length - 1) < 0 ||
// 获取当前线程对应的Cell
(a = as[getProbe() & m]) == null ||
// cells已经创建,且当前线程对应的Cell已创建,则直接CAS操作对应的Cell
!(uncontended = a.cas(v = a.value, v + x)))
longAccumulate(x, null, uncontended);
}
}
大致流程图如下:
如果存在资源争用的情况,会调用Striped64的
longAccumulate()方法,逻辑还是挺复杂的:
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
int h;//线程的哈希码
if ((h = getProbe()) == 0) {
// 如果线程的哈希码为0,说明没有被初始化
ThreadLocalRandom.current();// 强制初始化,计算threadLocalRandomProbe
// 获取线程的哈希码,即Thread.threadLocalRandomProbe
h = getProbe();
wasUncontended = true;
}
// 是否发生哈希碰撞:不同线程映射到了同一个Cell
boolean collide = false;
for (;;) {
Cell[] as; Cell a; int n; long v;
// cells已经完成初始化的情况
if ((as = cells) != null && (n = as.length) > 0) {
/*
(n - 1) & h就是取线程对应的Cell下标,和HashMap玩法一样
线程对应的Cell为null,则要创建Cell,加锁,放入cells中。
*/
if ((a = as[(n - 1) & h]) == null) {
if (cellsBusy == 0) { // 无锁时才能能操作
Cell r = new Cell(x); // 创建Cell实例
// 尝试加锁
if (cellsBusy == 0 && casCellsBusy()) {
// 加锁成功,将Cell实例放入cells,
boolean created = false;
try {// 加锁后再重新检查一遍
Cell[] rs; int m, j;
if ((rs = cells) != 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;
}
// wasUncontended代表上一次CAS操作是否成功
else if (!wasUncontended)
// CAS失败,则重置为true,后面重新计算哈希码
wasUncontended = true;
// 线程对应的Cell不为null,则尝试修改Cell的value值,成功则直接返回
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
// 当Cell[]长度达到CPU核心数,就不会再扩容了,计算密集型,再扩容意义不大
else if (n >= NCPU || cells != as)
collide = false; // At max size or stale
else if (!collide)
collide = true;
// 尝试加锁进行扩容操作
else if (cellsBusy == 0 && casCellsBusy()) {
try {
if (cells == as) {
// 两倍扩容
Cell[] rs = new Cell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
cells = rs;
}
} finally {
// 解锁
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
// 重新计算当前线程的哈希码,并写入Thread.threadLocalRandomProbe
h = advanceProbe(h);
}
// Cell[]未初始化的情况:尝试加锁并初始化
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
boolean init = false;
try { // Initialize table
if (cells == as) {
Cell[] rs = new Cell[2];// 默认长度是2
rs[h & 1] = new Cell(x);
cells = rs;
init = true;
}
} finally {
// 解锁
cellsBusy = 0;
}
if (init)
break;
}
// Cell[]正在初始化中,操作直接在base上完成
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break; // Fall back on using base
}
}
longAccumulate()主要有三种情况:
- Cell[]未初始化,则要加锁并初始化,默认长度为2。
- Cell[]在初始化的过程中,操作退回到
base变量中去完成,暂时直接修改base即可。 - Cell[]初始化完成,则判断和当前线程绑定的Cell是否已创建,如果没创建则加锁创建,并放入cells当中,如果已创建则直接操作
Cell.cas(),如果失败则说明多个线程在竞争同一个Cell,需要进行扩容操作。如果数组长度已经达到CPU的核心数,则不会再扩容了。
sum
由于添加的值被分散到了Cell[]中,因此求和时,需要将base和Cell[]中的每一项value做统计。
/*
由于求和时,其他线程仍在操作Cell,因此统计的结果只是一个估计值。
要想计算准确值,必须加全局锁。
*/
public long sum() {
Cell[] as = cells; Cell a;
long sum = base;//以base为基数,依次累加所有Cell的value即可
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
其他像increment()、decrement()、longValue()其实就是变相的调用了add()或sum(),这里就不作解释了。
总结
AtomicLong通过一个变量value作为long的实际值,所有线程的操作都会争用这一个资源,因此value会成为唯一的热点资源,并发量高时,会导致大量的CAS失败,线程会进行多次自旋重试,影响性能。
LongAdder通过分而治之的思想,将热点资源分散到了base和Cell[]中,当没有资源争用时,直接操作base即可,尽可能的将Cell[]的创建延迟。当确实发生资源争用时,才创建Cell[],默认长度是2,扩容规则是两倍,且最大长度是CPU的核心数。将Thread的threadLocalRandomProbe视为哈希码,通过hash & (cells.length-1)定位到Cell,每个线程只操作自己的Cell,降低资源的争用情况。当Cell还有剩余,但是Thread的哈希码冲突时,LongAdder会调用advanceProbe()重新计算Thread的哈希码,并写回到Thread.threadLocalRandomProbe。
分而治之、分段锁是LongAdder的核心思想。