AtomicLong 是不是该淘汰了?


Photo By Instagram sooyaaa

问题 5.

相信你一定记得学习并发编程的一个入门级例子,多个线程操作一个变量,累加 10000 次,最后结果居然不是 10000。后来你把这个变量换成了并发包中的原子类型变量 AtomicLong,完美的解决了并发问题。假如面试官问:还有更好的选择吗?LongAdder 了解过吗?你能对答如流吗?

我的答案

AtomicLong 是 Java 1.5 并发包中提供的一个原子类,他提供给了我们在多线程环境下安全的并发操作一个整数的特性。

并且性能还可以,它主要依赖了 2 个技术,volatile 关键字和 CAS 原子指令,不知道这俩个技术的小伙伴参考往期的文章: ReentranLock 实现原理居然是这样?


AtomicLong 性能已经不错了,但是当在线程高度竞争的状况下性能会急剧下降,因为高度竞争下 CAS 操作会耗费大量的失败计算,因为当一个线程去更新变量时候发现值早已经被其他线程更新了。
那么有没有更好的解决方案呢,于是 LongAdder 诞生了。

LongAdder 是 Java 1.8 并发包中提供的一个工具类,它采用了一个分散热点数据的思路。
简单来说,Atomic 中所有线程都去更新内存中的一个变量,而 LongAdder 中会有一个 Cell 类型的数组,这个数组的长度是 CPU 的核心数,因为一台电脑中最多同时会有 CPU 核心数个线程并行运行,每个线程更新数据时候会被映射到一个 Cell 元素去更新,这样就将原本一个热点的数据,分散成了多个数据,降低了热点,这样也就减少了线程的竞争程度,同时也就提高了更新的效率。

当然这样也带了一个问题,就是更新函数不会
返回 更新后的值,而 AtomicLong 的更新方法会返回更新后的结果,LongAdder 只有在调用 sum 方法的时候才会去累加每个 Cell 中的数据,然后返回结果。

当然 LongAdder 中也用到了volatile 和 CAS 原子操作,所以小伙伴们一定要掌握这俩个技术点,这是面试必问的点。

既然说 LongAdder 的效率更好,那我们就来一段测试代码,小小展示一下 LongAdder 的腻害之处,请看如下:

public class AtomicLongTester {


private static AtomicLong numA = new AtomicLong(0); private static LongAdder numB = new LongAdder();

public static void main(String[] args) throws InterruptedException { for (int i = 1; i < 10001; i*=10) { test(false, i); test(true, i); } }

public static void test(boolean isLongAdder, int threadCount) throws InterruptedException { long starTime = System.currentTimeMillis(); final CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) { new Thread(new Runnable() { public void run() { for (int i = 0; i < 100000; i++) { if (isLongAdder) { numB.add(1); } else { numA.addAndGet(1); } }
latch.countDown(); } }).start(); }
// 等待所有运算结束 latch.await();
if (isLongAdder) { System.out.println("Thread Count=" + threadCount + ", LongAdder cost ms=" + (System.currentTimeMillis() - starTime) + ", Result=" + numB.sum()); } else { System.out.println("Thread Count=" + threadCount + ", AtomicLong cost ms=" + (System.currentTimeMillis() - starTime) + ", Result=" + numA.get()); }
numA = new AtomicLong(0); numB = new LongAdder(); }
}

实验结果大致如下:

Thread Count=1, AtomicLong cost ms=9, Result=100000

Thread Count=1, LongAdder cost ms=13, Result=100000

Thread Count=10, AtomicLong cost ms=14, Result=1000000

Thread Count=10, LongAdder cost ms=41, Result=1000000

Thread Count=100, AtomicLong cost ms=111, Result=10000000

Thread Count=100, LongAdder cost ms=45, Result=10000000

Thread Count=1000, AtomicLong cost ms=1456, Result=100000000

Thread Count=1000, LongAdder cost ms=379, Result=100000000

Thread Count=10000, AtomicLong cost ms=17452, Result=1000000000

Thread Count=10000, LongAdder cost ms=3545, Result=1000000000

从上面的结果可以看出来,当线程竞争率比较低的时候 AtomicLong 效率还是优于 LongAdder 的,但是当线程竞争率增大的时候,我们可以看出来 LongAdder 的性能远远高于 AtomicLong。

因此在使用原子类的时候,我们要结合实际情况,如果竞争率很高,那么建议使用 LongAdder 来替代 AtomicLong。

说到 LongAdder 也不得的说一说 volatile 带来
伪共享问题,对伪共享感兴趣的同学欢迎关注后续的文章,我们会在后续的文章中探讨
这个 问题。

以上即为昨天的问题的答案,小伙伴们对这个答案是否满意呢?欢迎留言和我讨论。



又要到年末了,你是不是又悄咪咪的开始看机会啦。


为了广大小伙伴能充足电量,能顺利通过 BAT 的面试官无情三连炮,我特意推出大型刷题节目。


每天一道题目,第二天给答案,前一天给小伙伴们独立思考的机会。


点下“在看”,鼓励一下?