JDK12源码分析_04 LongAdder / DoubleAdder、LongAccumulator / DoubleAccumulator、Striped64 源码分析
LongAdder
AtomicLong内部只有一个volatile long value,这种非阻塞的原子操作虽然说相对原有的阻塞算法来说已经很好了,但是在高并发下多线程同时竞争一个原子变量的更新操作,由于同一时间只会有一个线程CAS操作成功,会造成大量的线程竞争失败后无限尝试重试。这无疑浪费了很多开销。JDK12的LongAdder,就能解决高并发的递增或递减的原子操作。
(图片来自网络,用于纯粹学习,如果侵权请联系作者删除。QQ邮箱:1393899065@qq.com)
public class LongAdder extends Striped64 implements Serializable {
在这里我们不得不介绍一下Striped64,这个强大的父类。Striped64有4个核心的变量。
abstract class Striped64 extends Number {
static final int NCPU = Runtime.getRuntime().availableProcessors();
transient volatile Cell[] cells;
transient volatile long base;
transient volatile int cellsBusy;
。。。。。。
NCPU 是获取当前所有CPU的核心数总和,这个在后面会介绍,是用来扩容用的。
Cell[] cells 和 long base 是保存数据的方法,他的思想就是把AtomicLong单个的原子对象拆成一个数组来操作,这样CAS失败的概率就很低了。那么我们就要提问了,它是怎么来计算总和的呢?
当LongAdder 初始化的时候并不会立刻使用到数组 Cell[] cells,而是直接使用base,只有发生CAS base 操作失败的时候才会初始化数组,所以说当前LongAdder 的值就是base+数组求和=总和。
public long sum() {
Cell[] cs = cells;
long sum = base;
if (cs != null) {
for (Cell c : cs)
if (c != null)
sum += c.value;
}
return sum;
}
但是我们发现这个求和的方法并没有锁,所以说求和是一个估计值。对数据准确性有要求的可以自己实现独占锁再求和。
@jdk.internal.vm.annotation.Contended static final class Cell {
volatile long value;
Cell(long x) { value = x; }
final boolean cas(long cmp, long val) {
return VALUE.compareAndSet(this, cmp, val);
}
}
这段代码是Cell对象的代码,我们可以看到每一个Cell在初始化的时候long value = 0,每一次的原子操作相当于是调用了单个数组对象的cas,也就是说如果失败重试那么仅仅只会影响这个数组对象。底层依旧是依赖MethodHandles.lookup()的VarHandle来实现CAS。
@jdk.internal.vm.annotation.Contended这个注解大家应该不陌生,为了避免CPU缓存行伪共享的问题。
final boolean casBase(long cmp, long val) {
return BASE.compareAndSet(this, cmp, val);
}
LongAdder 的默认操作是casBase,操作失败以后才会进行扩容Cell[] cells。
final boolean casCellsBusy() {
return CELLSBUSY.compareAndSet(this, 0, 1);
}
当然扩容操作是需要加锁的,cellsBusy状态CAS设置为1就是加锁,CAS设置为0就是解锁。接下来我们分析最核心的add入口。
public void add(long x) {
Cell[] cs; long b, v; int m; Cell c;
if ((cs = cells) != null || !casBase(b = base, b + x)) {
boolean uncontended = true;
if (cs == null || (m = cs.length - 1) < 0 ||
(c = cs[getProbe() & m]) == null ||
!(uncontended = c.cas(v = c.value, v + x)))
longAccumulate(x, null, uncontended);
}
}
可以看到如果cells为空,会直接casBase的操作,这里就有点类似AtomicLong了。如果casBase的操作失败了,就会去计算决定当前线程应该访问cells中的哪一个元素。如果计算出映射的元素存在就对该元素进行CAS操作,如果该元素不存在就会执行longAccumulate的复杂操作,其中对数组cells初始化的操作也包含在longAccumulate里面。longAccumulate的代码被封装在Striped64里面,这里我们重点分析一下。
// 陈涛版权所有,禁止转载,QQ邮箱:1393899065@qq.com
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
int h;
// 这个步骤是在初始化当前线程的变量threadLocalRandomProbe,
// 如果不了解 ThreadLocalRandom,可以把它暂时看成一个线程安全的随机数种子,
// 以后有篇幅在单独介绍ThreadLocalRandom
if ((h = getProbe()) == 0) {
ThreadLocalRandom.current(); // force initialization
h = getProbe();
wasUncontended = true;
}
boolean collide = false; // True if last slot nonempty
done: for (;;) {
Cell[] cs; Cell c; int n; long v;
if ((cs = cells) != null && (n = cs.length) > 0) {
if ((c = cs[(n - 1) & h]) == null) {
if (cellsBusy == 0) { // Try to attach new Cell
Cell r = new Cell(x); // Optimistically create
if (cellsBusy == 0 && casCellsBusy()) {
try { // Recheck under lock
Cell[] rs; int m, j;
if ((rs = cells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
break done;
}
} finally {
cellsBusy = 0;
}
continue; // Slot is now non-empty
}
}
collide = false;
}
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
// 这里判断如果cell存在就对当前元素执行CAS操作
else if (c.cas(v = c.value,
(fn == null) ? v + x : fn.applyAsLong(v, x)))
break;
// NCPU 是我们之前提到的CPU核心数量。
// 在扩容cells之前他会检查当前数组个数是否超过CPU核心数量,
// 因为他认为如果扩容超过CPU核心数量,只会造成更多的CAS失败是没有意义的。
// 当然这个扩容的操作仍然满足2的幂次方。
// n >= NCPU 是不会再进行扩容了。
else if (n >= NCPU || cells != cs)
collide = false; // At max size or stale
else if (!collide)
collide = true;
// 这里我们可以看出扩容要满足两个条件,
// 1个是对NCPU 的数量对比,第2个是当前元素操作有冲突,
// 然后采用casCellsBusy方法来加锁。
else if (cellsBusy == 0 && casCellsBusy()) {
try {
if (cells == cs) // Expand table unless stale
cells = Arrays.copyOf(cs, n << 1);
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
h = advanceProbe(h);
}
// 这里是初始化cells 数组
else if (cellsBusy == 0 && cells == cs && casCellsBusy()) {
try { // Initialize table
if (cells == cs) {
Cell[] rs = new Cell[2];
rs[h & 1] = new Cell(x);
cells = rs;
break done;
}
} finally {
cellsBusy = 0;
}
}
// Fall back on using base
else if (casBase(v = base,
(fn == null) ? v + x : fn.applyAsLong(v, x)))
break done;
}
}
volatile int cellsBusy 这个除了用来扩容的时候加锁,还可以用1和0来标识当前是否有可用的数组。假如当前线程把cellsBusy 设置成了1,那么其他的线程会直接去操作volatile long base的,不会去扩容的。
访问数组元素下标的计算方式,threadLocalRandomProbe & cells数组大小减1
private static final VarHandle BASE;
private static final VarHandle CELLSBUSY;
private static final VarHandle THREAD_PROBE;
这里可以看出所有的原子操作全部是通过VarHandle 来进行的,JDK新增加的类已经放弃了Unsafe的调用了。
DoubleAdder
DoubleAdder原理基本相似,就不再重复了。doubleAccumulate也是封装在Striped64里面的。我们来说一些不一样的地方。
public class DoubleAdder extends Striped64 implements Serializable {
我们可以看到DoubleAdder 继承了Striped64 ,但是我们刚才说了Striped64 里面的核心变量 volatile Cell[] cells、volatile long base,都是long 类型的。
Double类中doubleToRawLongBits,double类型占64位,而long类型也是占64位,两个类型在计算机中存储的机器码都是64位二进制,从0和1的角度来看,是没有任何区别的。区别在于,对应同一64位二进制机器码,两者的解析方式不同。
public double sum() {
Cell[] cs = cells;
double sum = Double.longBitsToDouble(base);
if (cs != null) {
for (Cell c : cs)
if (c != null)
sum += Double.longBitsToDouble(c.value);
}
return sum;
}
这里我们可以看到 longBitsToDouble 对类型做了转换。sum方法里面的Double.longBitsToDouble 把long类型转换成double,Striped64 doubleAccumulate里面的Double.doubleToRawLongBits 则是相反。
是不是两个特别像,所以才被命名为Striped64 的,换成其他的基本类型可能就不行了。
LongAccumulator
这两个类是LongAdder / DoubleAdder 的升级版。LongAdder 只能实现递增或者递减,假如我现在求乘法运算或者求最大值、求最小值呢?LongBinaryOperator 是一个双目运算器接口,可以实现这种自定义的计算功能。与之对应的还有 DoubleBinaryOperator
private final LongBinaryOperator function;
private final long identity;
public LongAccumulator(LongBinaryOperator accumulatorFunction,
long identity) {
this.function = accumulatorFunction;
base = this.identity = identity;
}
identity 在构造函数里是初始值。accumulatorFunction 则是需要传入的双目运算器。
// 陈涛版权所有,禁止转载,QQ邮箱:1393899065@qq.com
public static void main(String[] args) {
LongAccumulator my = new LongAccumulator(new LongBinaryOperator() {
public long applyAsLong(long left, long right) {
return Math.max(left, right);
}
}, 1);
my.accumulate(2L);
my.accumulate(3L);
System.out.println(my.get());
}
这里我们要先写一个例子,我们可以看到这里我们自己实现了 applyAsLong,求最大值算法。最后输出最大值:3
public long get() {
Cell[] cs = cells;
long result = base;
if (cs != null) {
for (Cell c : cs)
if (c != null)
result = function.applyAsLong(result, c.value);
}
return result;
}
我们可以看到get()就是把base和数组cells里面的所有值都做一次applyAsLong的运算,applyAsLong是自己实现的双目运算器。
DoubleAccumulator
DoubleAccumulator 稍有一些不一样。我们来看一下构造函数。
private final DoubleBinaryOperator function;
private final long identity; // use long representation
public DoubleAccumulator(DoubleBinaryOperator accumulatorFunction,
double identity) {
this.function = accumulatorFunction;
base = this.identity = doubleToRawLongBits(identity);
}
我们看到DoubleAccumulator 仍然是使用 long 类型来保存数据的,采用doubleToRawLongBits等方法来做类型转换。