likes
comments
collection
share

以为自己已经很快了 —— 聊聊AtomicLong

作者站长头像
站长
· 阅读数 26

大家好,我是徒手敲代码。

之前在这篇文章聊过,多线程环境下对共享变量进行修改操作,如果不想加锁,可以用 CAS,采用自旋的方式来避免加锁带来严重的性能损耗。Java 当中针对 CAS 的思想,设计了几个原子类,其中就包括 AtomicLong

这个类最常见的应用场景,包括计数器、序列生成器,以及需要在高并发环境下进行原子性更新长整型变量的场景。例如,在分布式系统中,它常被用来实现全局唯一ID的生成,或是统计服务的请求数量、成功响应次数等指标,确保在多线程或多进程访问时数据的准确性和一致性。

实现原理

查看 AtomicLong的源码,incrementAndGet()decrementAndGet()这两个方法,可以清楚看到,它是如何统计累加的:

    public final long incrementAndGet() {
        return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
    }

    public final long decrementAndGet() {
        return unsafe.getAndAddLong(this, valueOffset, -1L) - 1L;
    }

无论是加还是减,调用的都是同一个方法,只是传参的正负不一样,再点进去看getAndAddLong()方法:

    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;
    }

一直读取一个 volatile修饰的变量,保证这个值是最新的,然后尝试 CAS 自旋操作来更新变量的值,失败则一直重新尝试。

这样的做法有个问题,一旦线程数量很多,就会导致 CAS 长时间自旋,占用 CPU。比如有 n 个线程同时修改变量,那么就只有一个线程可以修改成功,而其他的 n-1 个线程需要自旋,然后重新尝试修改。

解决方案

针对上述提出的问题,jdk8中新设计了一个类LongAdder,可以在高并发场景下,更快地实现累加计数。

并且,在阿里巴巴手册中也明确提出:“推荐使用LongAdder对象,比AtomicLong性能更好(减少乐观锁的重试次数)”

下面通过一段小程序,来验证一下,是不是真的快:

分别测试了 1、10、100个线程,用AtomicLongLongAdder进行累加操作,所需要的时间

public class Demo {
    // 每个线程需要累加的次数
    private static final int NUM_ITERATIONS = 10000000

    public static void main(String[] args) {
        int[] threadCounts = {110100};

        for (int threadCount : threadCounts) {
            benchmarkAtomicLong(threadCount, NUM_ITERATIONS);
            benchmarkLongAdder(threadCount, NUM_ITERATIONS);
            System.out.println("----------------------------------------");
        }
    }

    private static void benchmarkAtomicLong(int threadCount, int iterations) {
        //固定线程数的线程池
        ExecutorService executor = Executors.newFixedThreadPool(threadCount);
        AtomicLong atomicLong = new AtomicLong(0L);

        long startTime = System.nanoTime();
        //线程池提交任务
        for (int i = 0; i < threadCount; i++) {
            executor.submit(() -> {
                for (int j = 0; j < iterations; j++) {
                    atomicLong.incrementAndGet();
                }
            });
        }

        executor.shutdown();
        while (!executor.isTerminated()) {}

        long endTime = System.nanoTime();
        System.out.printf("AtomicLong with %d threads took: %.2f seconds\n",
                threadCount, (endTime - startTime) / 1e9);
        System.out.println("Final Value of AtomicLong: " + atomicLong.get());
    }

    private static void benchmarkLongAdder(int threadCount, int iterations) {
        ExecutorService executor = Executors.newFixedThreadPool(threadCount);
        LongAdder longAdder = new LongAdder();

        long startTime = System.nanoTime();

        for (int i = 0; i < threadCount; i++) {
            executor.submit(() -> {
                for (int j = 0; j < iterations; j++) {
                    longAdder.increment();
                }
            });
        }

        executor.shutdown();
        while (!executor.isTerminated()) {}

        long endTime = System.nanoTime();
        System.out.printf("LongAdder with %d threads took: %.2f seconds\n",
                threadCount, (endTime - startTime) / 1e9);
        System.out.println("Final Value of LongAdder: " + longAdder.sum());
    }
}

打印结果:

AtomicLong with 1 threads took: 0.09 seconds
Final Value of AtomicLong: 10000000
LongAdder with 1 threads took: 0.08 seconds
Final Value of LongAdder: 10000000
----------------------------------------
AtomicLong with 10 threads took: 1.80 seconds
Final Value of AtomicLong: 100000000
LongAdder with 10 threads took: 0.16 seconds
Final Value of LongAdder: 100000000
----------------------------------------
AtomicLong with 100 threads took: 15.66 seconds
Final Value of AtomicLong: 1000000000
LongAdder with 100 threads took: 1.84 seconds
Final Value of LongAdder: 1000000000
----------------------------------------

可以看出,1个线程执行的时候,因为不存在竞争,所以两个结果差不多,一旦线程数量多了起来,结果就很明显,LongAdder要比AtomicLong快很多。

今天的分享到这里结束了。

关注公众号“徒手敲代码”,免费领取腾讯大佬推荐的Java电子书!

转载自:https://juejin.cn/post/7376950812059107363
评论
请登录