关于java:阿里为什么推荐使用LongAdder而不是volatile

35次阅读

共计 3136 个字符,预计需要花费 8 分钟才能阅读完成。


阿里《Java 开发手册》最新嵩山版在 8.3 日公布,其中有一段内容引起了老王的留神,内容如下:

【参考】volatile 解决多线程内存不可见问题。对于一写多读,是能够解决变量同步问题,然而如果多写,同样无奈解决线程平安问题。

阐明:如果是 count++ 操作,应用如下类实现:AtomicInteger count = new AtomicInteger();
count.addAndGet(1); 如果是 JDK8,举荐应用 LongAdder 对象,比 AtomicLong 性能更好(缩小乐观
锁的重试次数)。

以上内容共有两个重点:

  1. 相似于 count++ 这种非一写多读的场景不能应用 volatile
  2. 如果是 JDK8 举荐应用 LongAdder 而非 AtomicLong 来代替 volatile,因为 LongAdder 的性能更好。

但口说无凭,即便是孤尽大佬说的,咱们也得证实一下,因为马老爷子说过:实际是测验真谛的唯一标准

这样做也有它的益处,第一,加深了咱们对常识的认知;第二,文档上只写了LongAdderAtomicLong 的性能高,然而高多少呢?文中并没有说,那只能咱们本人入手去测试喽。

话不多,接下来咱们间接进入本文正式内容 …

volatile 线程平安测试

首先咱们来测试 volatile 在多写环境下的线程平安状况,测试代码如下:

public class VolatileExample {
    public static volatile int count = 0; // 计数器
    public static final int size = 100000; // 循环测试次数

    public static void main(String[] args) {
        // ++ 形式 10w 次
        Thread thread = new Thread(() -> {for (int i = 1; i <= size; i++) {count++;}
        });
        thread.start();
        // -- 10w 次
        for (int i = 1; i <= size; i++) {count--;}
        // 等所有线程执行实现
        while (thread.isAlive()) {}
        System.out.println(count); // 打印后果
    }
}

咱们把 volatile 润饰的 count 变量 ++ 10w 次,在启动另一个线程 — 10w 次,失常来说后果应该是 0,然而咱们执行的后果却为:

1063

论断:由以上后果能够看出 volatile 在多写环境下是非线程平安的,测试后果和《Java 开发手册》相吻合。

LongAdder VS AtomicLong

接下来,咱们应用 Oracle 官网的 JMH(Java Microbenchmark Harness,JAVA 微基准测试套件)来测试一下两者的性能,测试代码如下:

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.LongAdder;

@BenchmarkMode(Mode.AverageTime) // 测试实现工夫
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS) // 预热 1 轮,每次 1s
@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) // 测试 5 轮,每次 3s
@Fork(1) // fork 1 个线程
@State(Scope.Benchmark)
@Threads(1000) // 开启 1000 个并发线程
public class AlibabaAtomicTest {public static void main(String[] args) throws RunnerException {
        // 启动基准测试
        Options opt = new OptionsBuilder()
                .include(AlibabaAtomicTest.class.getSimpleName()) // 要导入的测试类
                .build();
        new Runner(opt).run(); // 执行测试}

    @Benchmark
    public int atomicTest(Blackhole blackhole) throws InterruptedException {AtomicInteger atomicInteger = new AtomicInteger();
        for (int i = 0; i < 1024; i++) {atomicInteger.addAndGet(1);
        }
        // 为了防止 JIT 疏忽未被应用的后果
        return atomicInteger.intValue();}

    @Benchmark
    public int longAdderTest(Blackhole blackhole) throws InterruptedException {LongAdder longAdder = new LongAdder();
        for (int i = 0; i < 1024; i++) {longAdder.add(1);
        }
        return longAdder.intValue();}
}

程序执行的后果为:

从上述的数据能够看出,在开启了 1000 个线程之后,程序的 LongAdder 的性能比 AtomicInteger 快了约 1.53 倍,你没看出是开了 1000 个线程,为什么要开这么多呢?这其实是为了模仿高并发高竞争的环境下二者的性能查问。

如果在低竞争下,比方咱们开启 100 个线程,测试的后果如下:

论断:从下面后果能够看出,在低竞争的并发环境下 AtomicInteger 的性能是要比 LongAdder 的性能好,而高竞争环境下 LongAdder 的性能比 AtomicInteger,当有 1000 个线程运行时,LongAdder 的性能比 AtomicInteger 快了约 1.53 倍,所以各位要依据本人业务状况抉择适合的类型来应用。

性能剖析

为什么会呈现下面的状况?这是因为 AtomicInteger 在高并发环境下会有多个线程去竞争一个原子变量,而始终只有一个线程能竞争胜利,而其余线程会始终通过 CAS 自旋尝试获取此原子变量,因而会有肯定的性能耗费;而 LongAdder 会将这个原子变量拆散成一个 Cell 数组,每个线程通过 Hash 获取到本人数组,这样就缩小了乐观锁的重试次数,从而在高竞争下取得劣势;而在低竞争下体现的又不是很好,可能是因为本人自身机制的执行工夫大于了锁竞争的自旋工夫,因而在低竞争下体现性能不如 AtomicInteger

总结

本文咱们测试了 volatile 在多写状况下是非线程平安的,而 在低竞争的并发环境下 AtomicInteger 的性能是要比 LongAdder 的性能好,而高竞争环境下 LongAdder 的性能比 AtomicInteger,因而咱们在应用时要联合本身的业务状况来抉择相应的类型。

关注公众号「Java 中文社群」发送“面试”,支付最新整顿的面试材料。

正文完
 0