LongAdder原理分析和性能测试

介绍

LongAddrJDK1.8才有的。其在高并发情况下,相比与AtomicLong的性能更高。本篇主要分析一下其实现原理。并且与AtomicLong做一个性能对比测试。

AtomicLong利用CPUCAS实现的原子化指令实现。

public final long getAndAddLong(Object var1, long var2, long var4) {
    long var6;
    do {
        var6 = this.getLongVolatile(var1, var2);// 先从内存中拿到最新值
      // 做比较并交换,(内存地址,偏移量,最新值,准备更新的值)
    } while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));
    return var6;
}

以上代码是CAS算法的源码。有一个do while的自旋操作。如果并发量过大。会导致自旋的次数过多。那么性能就下来了。大家都在集中抢占成员变量value。同一时间有上百个线程在抢value的更新权限。那不得。。。

既然大家争抢同一资源过于凶猛,狼多肉少,那就多来点肉呗。

value值分一下,分成比如4份。比如value=4,分成4份之后,每份都是1,[1,1,1,1],这个时候想要累加1,只需要找到4个值中的一个,累加1即可,如果正好有4个线程的话,各加个的,互不打扰。value = 1+1+1+2 = 5。跟我们想要的结果是一致的。这样,原来的竞争压力就被分散,相当于降低到了原来的1/4。相信这个原理不难理解。

来分析一下源代码:

// LongAddr.add
public void add(long x) {
    Cell[] as; long b, v; int m; Cell a;
    if ((as = cells) != null || !casBase(b = base, b + x)) {
        boolean uncontended = true;
        if (as == null || (m = as.length - 1) < 0 ||
            (a = as[getProbe() & m]) == null ||
            !(uncontended = a.cas(v = a.value, v + x)))
            longAccumulate(x, null, uncontended);
    }
}

上面这个类真的挺晦涩的。我把它愿意调整下。看的舒服些。愿意没有变化。

public void add(long x) {
    Cell[] as; // Cell[]对象是用来存储上文说的value拆分之后的数据的对象
    long b, v;
    int m;
    Cell a;

    // 如果cells没有初始化并且通过CAS计算base值成功了,就不用拆分了。直接离开
    if ((as = cells) == null && casBase(b = base, b + x)) {
        return;
    }

    boolean uncontended = true;
    boolean enterAccumulate =
            (as == null)    // cells是否初始化
            || (m = as.length - 1) < 0// cells数组的长度是否大于1
            || (a = as[getProbe() & m]) == null// 从cells中拿一个值来看看是否为null
            || !(uncontended = a.cas(v = a.value, v + x));// 尝试对cells中的一个值做累加

    if (enterAccumulate) {
        longAccumulate(x, null, uncontended);
    }
}

先说下LongAddr的设计原则

1.首先使用base来存储原值,在低并发状态下,优先使用对原值baseCAS操作,如果能成功,那么尽量不要使用Cell[]数组,因为存储也是有代价的。因为LongAddr的性能代价是用存储换来的。

2.一旦出现了对base原值的CAS更新操作失败,说明有竞争了,那么就开始启用Cell[],拆分原值。

3.一旦开始使用Cell[]数组,就回不去了。以后一直都会使用下去。

综合上述原则,解释下上面这段代码(Cell这个对象自己看下源码就能理解)

1.首先检查cells是否初始化,如果没有就对base原值做CAS操作,如果成功则离开(说明没有竞争)

2.如果对base原值做CAS操作失败,开始启用Cell[]

3.longAccumulate方法是用来做cell数组的初始化和扩容的。有4个条件来判断是否要进入longAccumulate方法

说下4条件(4个条件只要一个为true就会进入longAccumulate

as == null,说明没有初始化呢,那就进去初始化呗

(m = as.length - 1) < 0 => as.length < 1,也是说明没有初始化,或者初始化到一半,那就进去初始化呗

(a = as[getProbe() & m]) == null,拿一个值出来看看是否为空,说明也要进去初始化一下

!(uncontended = a.cas(v = a.value, v + x),尝试对数组中的这个cell做累加操作,如果成功就完事,如果失败,说明竞争还是很激烈,那就扩容呗。所以进去扩容下。

下面说说longAccumulate方法,不得不说这个方法真的很晦涩,难懂。哎。。。硬着头皮看吧。

final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
    int h;
    if ((h = getProbe()) == 0) {
        ThreadLocalRandom.current(); // force initialization
        h = getProbe();
        wasUncontended = true;
    }
    boolean collide = false;                // True if last slot nonempty
    for (; ; ) {
        Cell[] as;
        Cell a;
        int n;
        long v;
        if ((as = cells) != null && (n = as.length) > 0) {// cells已经初始化

        } else if (cellsBusy == 0 && cells == as && casCellsBusy()) {// cells还没有初始化,并且也没有其他线程在做初始化动作,cellsBusy == 0表示未上锁

        } else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x)))) {// 有其他线程在做初始化或者扩容动作,这边直接尝试对base原值做累加动作

        }
    }
}

我把主分支拉出来了,有3个分支。把顺序颠倒下,好理解。

1.if (cellsBusy == 0 && cells == as && casCellsBusy())

cells还没有初始化,并且也没有其他线程在做初始化动作,cellsBusy == 0表示未上锁

2.if ((as = cells) != null && (n = as.length) > 0)

cells已经初始化

3.if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x))))

有其他线程在做初始化或者扩容动作,这边直接尝试对base原值做累加动作

1.看下初始化动作

boolean init = false;
try {                           // Initialize table
    if (cells == as) {
        Cell[] rs = new Cell[2];// 先来2个的数组,不够后面再扩容
        rs[h & 1] = new Cell(x);// 0,1随便来一个初始化了
        cells = rs;
        init = true;
    }
} finally {
    cellsBusy = 0;// 解锁
}
if (init)
    break;

2.看下已经初始化之后的动作

if ((a = as[(n - 1) & h]) == null) {// 选出来的那个cell为null
    if (cellsBusy == 0) {       // 确认cell数组没有在做扩容操作
        Cell r = new Cell(x);   // new Cell
        if (cellsBusy == 0 && casCellsBusy()) {// 加锁准备把cell对象塞进数组
            boolean created = false;
            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;// 把刚刚创建的cell对象塞进数组
                    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 (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x))))// 尝试做CAS更新动作,如果失败就继续循环呗
    break;
else if (n >= NCPU || cells != as)// cells的容量有上线,一般最多是等于,应该不会超过。写上大于估计是为了以防万一
    collide = false;            // 不再扩容
else if (!collide)
    collide = true;
else if (cellsBusy == 0 && casCellsBusy()) {// 扩容,上锁
    try {
        if (cells == as) {      // Expand table unless stale
            Cell[] rs = new Cell[n << 1];// 扩容成2倍
            for (int i = 0; i < n; ++i)// 逐个赋值
                rs[i] = as[i];
            cells = rs;
        }
    } finally {
        cellsBusy = 0;// 扩容结束,解锁
    }
    collide = false;
    continue;                   // Retry with expanded table
}
h = advanceProbe(h);

最后看下LongAddr怎么取值

public long sum() {
    Cell[] as = cells; Cell a;
    long sum = base;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}

这段不难看明白,说白了就是全部累加起来呗。但是在高并发时,它与真实值有一定的差距。把原来的一个值分散成8个或者16个。那么你做sum操作时,各个线程还在不停修改值。分的越多,误差越大。

看下类图结构

image

除了LongAddr还有其他几个类。原理基本是类似的。可以自己看源码。

总结

并发大师的思路真的很牛逼,但是这源码吧,我也不敢说不好,可就是觉得晦涩,看着费劲。

性能测试

为了更形象的提现LongAddr高性能,我把AtomicLong拿过来做了一下性能对比测试。

测试工具:OpenJdkBenchMark工具,JMH

系统硬件资源:Mac OS,I7 4核CPU

看下测试代码:

@State(value = Scope.Benchmark)
public class BenchMarkTest {

    private static final LongAdder LONG_ADDER_VALUE = new LongAdder();
    private static final AtomicLong ATOMIC_LONG_VALUE = new AtomicLong(0);

    @Param(value = {"10"})
    private int thread;

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(BenchMarkTest.class.getSimpleName())
                .warmupIterations(3)// 预热3轮
                .measurementTime(TimeValue.seconds(1))
                .measurementIterations(5)// 度量5轮,总共测试5轮来度量性能
                .forks(1)
                .threads(10)
                .result("result.json")
                .resultFormat(ResultFormatType.JSON)
                .build();
        new Runner(opt).run();
    }

    @Benchmark
    @BenchmarkMode({Mode.AverageTime})
    @OutputTimeUnit(TimeUnit.MILLISECONDS)
    public void longAddrIncrementTest() {
        for (int i = 0; i < 100000; i++) {
            LONG_ADDER_VALUE.increment();
        }
    }

    @Benchmark
    @BenchmarkMode({Mode.AverageTime})
    @OutputTimeUnit(TimeUnit.MILLISECONDS)
    public void atomicLongIncrementTest() {
        for (int i = 0; i < 100000; i++) {
            ATOMIC_LONG_VALUE.incrementAndGet();
        }
    }
}

每次测试预热3轮,度量测试5轮。每轮1秒钟。分别在(1,3,5,10个并发)情况下查看吞吐量和响应时间。

没一轮测试都是把一个long类型的值从0累加到10万。

因为这个操作是纯粹的CPU密集型工作,所以线程量没必要上到很高。

下面看测试结果。

红色:AtomicLong的运行状况

蓝灰色:LongAddr的运行状况

image

上图是吞吐量,ops/s,可以看出随着线程的增长,LongAddr的优势非常明显。10线程时,相差13倍。

image

上图是响应时间,单位ms/op,可以看出随着线程的增长,LongAddr的优势非常明显。10线程时,相差13倍。

你可能感兴趣的:(LongAdder原理分析和性能测试)