JDK12源码分析_04 LongAdder / DoubleAdder、LongAccumulator / DoubleAccumulator、Striped64 源码分析

深入剖析JDK12中LongAdder、DoubleAdder、LongAccumulator、DoubleAccumulator及Striped64的源码,揭示其如何解决高并发场景下原子操作的效率问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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等方法来做类型转换。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值