阿里为什么推荐使用LongAdder,而不是volatile?

简介: 阿里为什么推荐使用LongAdder,而不是volatile?

阿里《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();
    }
}


程序执行的结果为:

image.png


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

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

image.png

结论:从上面结果可以看出,在低竞争的并发环境下 AtomicInteger 的性能是要比 LongAdder 的性能好,而高竞争环境下 LongAdder 的性能比 AtomicInteger,当有 1000 个线程运行时,LongAdder 的性能比 AtomicInteger 快了约 1.53 倍,所以各位要根据自己业务情况选择合适的类型来使用。


性能分析

为什么会出现上面的情况?这是因为 AtomicInteger 在高并发环境下会有多个线程去竞争一个原子变量,而始终只有一个线程能竞争成功,而其他线程会一直通过 CAS 自旋尝试获取此原子变量,因此会有一定的性能消耗;而 LongAdder 会将这个原子变量分离成一个 Cell 数组,每个线程通过 Hash 获取到自己数组,这样就减少了乐观锁的重试次数,从而在高竞争下获得优势;而在低竞争下表现的又不是很好,可能是因为自己本身机制的执行时间大于了锁竞争的自旋时间,因此在低竞争下表现性能不如 AtomicInteger


总结

本文我们测试了 volatile 在多写情况下是非线程安全的,而在低竞争的并发环境下 AtomicInteger 的性能是要比 LongAdder 的性能好,而高竞争环境下 LongAdder 的性能比 AtomicInteger,因此我们在使用时要结合自身的业务情况来选择相应的类型。

相关文章
|
6月前
|
存储 缓存 Java
volatile底层原理详解
volatile底层原理详解
58 0
|
6月前
|
安全 Java 测试技术
解密Java并发中的秘密武器:LongAdder与Atomic类型
解密Java并发中的秘密武器:LongAdder与Atomic类型
325 1
|
6月前
|
Java 编译器
多线程(volatile)
多线程(volatile)
29 0
|
6月前
|
存储 安全 Java
【亮剑】Java并发编程涉及`ThreadLocal`、`Volatile`、`Synchronized`和`Atomic`四个关键机制
【4月更文挑战第30天】Java并发编程涉及`ThreadLocal`、`Volatile`、`Synchronized`和`Atomic`四个关键机制。`ThreadLocal`为每个线程提供独立变量副本;`Volatile`确保变量可见性,但不保证原子性;`Synchronized`实现同步锁,保证单线程执行;`Atomic`类利用CAS实现无锁并发控制。理解其原理有助于编写高效线程安全代码。根据业务场景选择合适机制至关重要。
42 0
|
6月前
|
设计模式 算法 安全
阿里推荐 LongAdder ,不推荐 AtomicLong !
在分布式系统中,计数器是一个常见的需求。为了实现高并发、高可用的计数器,我们需要选择一个合适的实现方式。在 Java 中,有两种常见的计数器实现方式:AtomicLong 和 LongAdder。最近,阿里巴巴在一份技术报告中推荐使用 LongAdder ,而不是 AtomicLong。本文将介绍这两种计数器的原理和优缺点,并分析为什么阿里巴巴推荐使用 LongAdder。
114 3
|
SQL 缓存 安全
synchronized和volatile底层原理分析
CAS Compare And Swap (Compare And Exchange) / 自旋 / 自旋锁 / 无锁 因为经常配合循环操作,直到完成为止,所以泛指一类操作 cas(v, a, b)
91 0
|
缓存 Java
Java并发编程系列2 - volatile
主要讲解volatile的相关知识,以及容易遇到的坑。
166 1
Java并发编程系列2 - volatile
|
安全 Java 开发者
京东面试官:说说synchronized 和 ReentrantLock的区别!
前面我们介绍了很多关于多线程的内容,在多线程中有一个很重要的课题需要我们攻克,那就是线程安全问题。线程安全问题指的是在多线程中,各线程之间因为同时操作所产生的数据污染或其他非预期的程序运行结果。
128 0
|
存储 SQL 安全
面试:为了进阿里,重新翻阅了Volatile与Synchronized
在深入理解使用Volatile与Synchronized时,应该先理解明白Java内存模型 (Java Memory Model,JMM)
164 0
面试:为了进阿里,重新翻阅了Volatile与Synchronized
|
Java 编译器
volatile与synchronized 区别
volatile与synchronized 区别
129 0