AtomicXXX 用得好好的,阿里为什么推荐使用 LongAdder?面试必问

简介: 面试连环炮先来一连炮简单的面试,看你能顶住几轮?栈长:1、多线程情况下,进行数字累加(count++)要注意什么?张三:要注意给累加方法加同步锁,不然会出现变量可见性问题,变量值被其他线程覆盖出现不一致的情况栈长:2、保证变量的可见性,用 volatile 修饰不就行了吗?

面试连环炮

先来一连炮简单的面试,看你能顶住几轮?


栈长:

1、多线程情况下,进行数字累加(count++)要注意什么?

张三:

要注意给累加方法加同步锁,不然会出现变量可见性问题,变量值被其他线程覆盖出现不一致的情况

栈长:

2、保证变量的可见性,用 volatile 修饰不就行了吗?

张三:

volatile 是可以保证可见性,但不能保证原子性和线程安全

栈长:

3、除了加同步锁这种方案,还有别的方法吗?

张三:

还可以用 JDK 中的原子类,比如:AtomicInteger、AtomicLong,它们是通过 CAS 算法实现的一种乐观主义

栈长:

4、不错,还知道别的么?

张三:

呃……


认真的,你能顶住几轮?

这些问题是 Java 程序员面试过程中必问的,出场率贼高,Java 程序员必懂,这些题在Java面试库小程序中也都有详细答案,这里就不展开了。

你还知道别的么?最后一轮的答案就是今天的主题!

更好的选择:LongAdder

你还在用 AtomicInteger、AtomicLong 原子类进行并发累加操作吗?那你就 OUT 了!

除了 AtomicInteger、AtomicLong,其实在 JDK 8 中更建议使用 LongAdder 进行原子性操作,性能更好,如果你使用的还是 JDK 7-,那当我没说,即使如此,也不能找借口不知道,毕竟 JDK 8 是现在的主流应用版本了。

阿里巴巴最新的 Java开发手册 是这么定义的:

网络异常,图片无法展示
|

这份阿里巴巴完整的 Java 开发手册,可以关注公众号:Java核心技术,回复:手册,即可下载高清完整版。

如果你还没有用过 LongAdder,不妨看看本文,刷新你的认知,栈长带你涨知识!

为什么搞出了 LongAdder?

我们都知道在 JDK 5 中搞出了 AtomicInteger、AtomicLong 等原子类,这也是在 JDK 8 之前普遍用的原子性操作类,来看下 AtomicLong 的累加源码:

网络异常,图片无法展示
|

大家都知道这些原子类都是通过 CAS 算法实现的乐观锁,通过旧值和现有的值不断循环比对,直到比对成功才修改成功结束循环。

这样就会有一个问题,如果并发数很高的话,就会造成过多的没有必要的 "循环",这势必会影响 CPU 的性能。

所以,JDK 8 又搞出来了一个 LongAdder,也在 atomic 包下:

网络异常,图片无法展示
|

大家可以看到,在同级宝箱中还有一个 LongAccumulator 类,这个这篇不展开,栈长下次再另开一篇具体分析,关注公众号:Java技术栈,写完我会第一时间进行推送。

LongAdder 为什么性能更好?

来分析下 LongAdder 类的源码:

网络异常,图片无法展示
|

累加

在 LongAdder 中维护了一个 Cell 数组,当 Cell 它不为空时,size 是 2 的次幂大小,每个 Cell 数组里面都有一个初始值为 0 的 long 变量,用来存储每个 Cell 的值:

网络异常,图片无法展示
|

Cell 类源码

然后其中的 sum 方法用来对 Cell 数组进行求和再加上 base 基础值进行返回:

网络异常,图片无法展示
|

求和

关于 base 基础值:

LongAdder 并不会一开始就创建 Cell 数组,其本身也会维护一个 base 基础值,当 CAS 更新失败时才进行创建或者扩容。

来看下 AtomicXXX 和 LongAdder 更新对比图:

网络异常,图片无法展示
|

来源:https://acet.pe.kr/809

Cell 数组相当于一个分段的概念,把 AtomicXXX 中的一个值分成了多个值进行管理,当 CAS 更新失败时不再当前循环重试,而是尝试获取其他的资源锁,这样就降低了对于 AtomicXXX 中间是单个资源的竞争,所以 LongAdder 的性能更高。

虽然 LongAdder 性能更好,那有没有缺点呢?

LongAdder 带来了良好的性能,代价肯定也是有的,既然维护了 Cell 数组,也就意味着要占用更多的内存空间,以空间换时间,也是值得的。

实战测试

既然官方都说在高并发的情况下性能更好,事实是否如此呢?

栈长必须实战测试一翻,打消大家的疑虑!

AtomicLong 测试代码:

/**
 * @author: 栈长
 * @from: 公众号Java技术栈
 */
private static void atomicLongTest() throws InterruptedException {
    long start = System.currentTimeMillis();
    ExecutorService es = Executors.newFixedThreadPool(MAX_POOL_SIZE);
    for (int i = 0; i < MAX_POOL_SIZE; i++) {
        es.execute(() -> {
            for (int j = 0; j < MAX_LOOP_SIZE; j++) {
                atomicLong.incrementAndGet();
            }
        });
    }
    es.shutdown();
    es.awaitTermination(5, TimeUnit.MINUTES);
    System.out.printf("AtomicLong %s*%s 结果:%s,耗时:%sms.\n",
            MAX_POOL_SIZE,
            MAX_LOOP_SIZE,
            atomicLong.get(),
            (System.currentTimeMillis() - start));
}

LongAdder 测试代码:

/**
 * @author: 栈长
 * @from: 公众号Java技术栈
 */
private static void longAdderTest() throws InterruptedException {
    long start = System.currentTimeMillis();
    ExecutorService es = Executors.newFixedThreadPool(MAX_POOL_SIZE);
    for (int i = 0; i < MAX_POOL_SIZE; i++) {
        es.execute(() -> {
            for (int j = 0; j < MAX_LOOP_SIZE; j++) {
                longAdder.increment();
            }
        });
    }
    es.shutdown();
    es.awaitTermination(5, TimeUnit.MINUTES);
    System.out.printf("LongAdder %s*%s 结果:%s,耗时:%sms.\n",
            MAX_POOL_SIZE,
            MAX_LOOP_SIZE,
            longAdder.sum(),
            (System.currentTimeMillis() - start));
}

这里只贴核心测试代码了,完整代码已上传到了 Github:

https://github.com/javastacks/javastack

测试结果:

网络异常,图片无法展示
|

这里测试的只有是 1 个线程,每个线程循环累加 1 次,这个没有锁竞争、没有高并发操作的场景就能看出性能上的差异了。。

栈长再不断提升 线程数、循环累加次数 ,得到了以下测试结果:

线程数 * 循环次数AtomicLongLongAdder1 * 145ms1ms10 * 1055ms2ms10 * 10056ms2ms100 * 1058ms10ms100 * 10074ms10ms1000 * 10190ms71ms1000 * 100217ms73ms1000 * 1000194ms81ms1000 * 10000301ms114ms1000 * 1000001813ms277ms1000 * 100000017596ms1629ms

图表对比:

网络异常,图片无法展示
|

从测试结果可以看出,LongAdder 的性能都是碾压 AtomicLong 的,最高可达 28 多倍的差距(56/2),可以说在高性能要求的高并发场景,肯定是有必要用 LongAdder 的,这也是阿里巴巴为什么建议使用 LongAdder 的原因。

当然,这只是我个人的测试,这个也和硬件配置有关系的,但毋庸置疑是,AtomicLong 的性能是更好的。另外,栈长写的 Java 多线程系列教程全部整理好了,微信搜索Java技术栈,在后台发送:Java,可以在线阅读。

总结

本文以一场面试连环炮揭开了 LongAdder 的面纱,怎么解决 count++ 的线程安全性问题?

栈长再总结下:

  • 累加方法加 synchronized/ Lock 同步锁;
  • 使用 AtomicInteger/ AtomicLong 原子类;
  • 使用 LongAdder 原子类(推荐使用);

LongAdder 这个东西是 Java 8 搞出来的,用来代替 AtomicXXX,不管是否高并发场景,都完胜 AtomicXXX,它不仅可以改善性能,现在面试也问的越来越多了,大家还是有必要掌握。

虽然 LongAdder 性能更好,但也是以更多的内存空间消耗为代价的,当然,现代计算机,内存早已不是瓶颈,所以这点消耗是可以忽略不计的,性能还是最重要的,但是大家也要知道这个点。

本文实战源代码完整版已经上传:

https://github.com/javastacks/javastack

好了,今天的分享就到这里了!

相关文章
|
7月前
|
Java 程序员
java线程池讲解面试
java线程池讲解面试
100 1
|
4月前
|
缓存 安全 Java
一文吃透面试线程必问10大问题
本文全面探讨了Java线程的十个关键面试问题,涵盖了线程的基本概念、创建方法、使用目的与好处、运行流程与状态、停止线程的正确方式、以及线程安全等高级主题。
|
7月前
|
存储 缓存 安全
2023春招面试题:Java并发相关知识
2023春招面试题:Java并发相关知识
|
架构师 云计算 开发者
架构师面试必问问题
架构师面试必问问题
|
监控 Java
带着面试官畅游Java线程池
java中经常需要用到多线程来处理一些业务,如果单纯使用继承Thread或者实现Runnable接口的方式来创建线程,那样势必有创建及销毁线程耗费资源、线程上下文切换问题。同时创建过多的线程也可能引发资源耗尽的风险,这个时候引入线程池比较合理,方便线程任务的管理。
带着面试官畅游Java线程池
|
SQL 缓存 Java
阿里面试官让我讲讲volatile,我直接从HotSpot开始讲起,一套组合拳拿下面试
阿里面试官让我讲讲volatile,我直接从HotSpot开始讲起,一套组合拳拿下面试
阿里面试官让我讲讲volatile,我直接从HotSpot开始讲起,一套组合拳拿下面试
AtomicXXX 用得好好的,阿里为什么推荐使用 LongAdder?面试必问
面试连环炮 先来一连炮简单的面试,看你能顶住几轮? 栈长: 1、多线程情况下,进行数字累加(count++)要注意什么? 张三: 要注意给累加方法加同步锁,不然会出现变量可见性问题,变量值被其他线程覆盖出现不一致的情况
|
存储 算法 Java
【直击面试】JVM这些面试题你都会了吗
查漏补缺JVM面试题 👀小提示:有些干,谨慎食用!!!!
【直击面试】JVM这些面试题你都会了吗
|
存储 监控 安全
我是这样给阿里面试官吹 ConcurrentHashMap的
因为上篇文章HashMap已经讲解的很详细了,因此此篇文章会简单介绍思路,再学习并发HashMap就简单很多了,上一篇文章中我们最终知道HashMap是线程不安全的,因此在老版本JDK中...
161 0
|
Java
和阿里面试官对线FutureTask源码面试(下)
和阿里面试官对线FutureTask源码面试(下)
129 0
和阿里面试官对线FutureTask源码面试(下)