一、LongAdder简介
JDK1.8时,java.util.concurrent.atomic
包中提供了一个新的原子类:LongAdder
。
根据Oracle官方文档的介绍,LongAdder在高并发的场景下会比它的前辈————AtomicLong 具有更好的性能,代价是消耗更多的内存空间:
那么,问题来了:
为什么要引入
LongAdder
?AtomicLong
在高并发的场景下有什么问题吗? 如果低并发环境下,LongAdder
和AtomicLong
性能差不多,那LongAdder
是否就可以替代AtomicLong
了?
1.1 为什么要引入LongAdder?
我们知道, AtomicLong 是利用了底层的CAS操作来提供并发性的,比如 addAndGet 方法:
上述方法调用了 Unsafe 类的 getAndAddLong 方法,该方法是个 native 方法,它的逻辑是采用自旋的方式不断更新目标值,直到更新成功。
在并发量较低的环境下,线程冲突的概率比较小,自旋的次数不会很多。但是,高并发环境下,N个线程同时进行自旋操作,会出现大量失败并不断自旋的情况,此时 AtomicLong 的自旋会成为瓶颈。
这就是 LongAdder 引入的初衷——解决高并发环境下 AtomicLong 的自旋瓶颈问题。
1.2 LongAdder快在哪里?
既然说到 LongAdder 可以显著提升高并发环境下的性能,那么它是如何做到的?这里先简单的说下 LongAdder 的思路,第二部分会详述 LongAdder 的原理。
我们知道, AtomicLong 中有个内部变量 value 保存着实际的long值,所有的操作都是针对该变量进行。也就是说,高并发环境下,value变量其实是一个热点,也就是N个线程竞争一个热点。
LongAdder 的基本思路就是 分散热点 ,将value值分散到一个数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。
这种做法有没有似曾相识的感觉?没错,[ConcurrentHashMap][1]中的“分段锁”其实就是类似的思路。
1.3 LongAdder能否替代AtomicLong?
回答这个问题之前,我们先来看下 LongAdder 提供的API:
可以看到, LongAdder 提供的API和 AtomicLong 比较接近,两者都能以原子的方式对long型变量进行增减。
但是 AtomicLong 提供的功能其实更丰富,尤其是 addAndGet 、 decrementAndGet 、 compareAndSet 这些方法。
addAndGet 、 decrementAndGet 除了单纯的做自增自减外,还可以立即获取增减后的值,而 LongAdder 则需要做同步控制才能精确获取增减后的值。如果业务需求需要精确的控制计数,做计数比较, AtomicLong 也更合适。
另外,从空间方面考虑, LongAdder 其实是一种“空间换时间”的思想,从这一点来讲 AtomicLong 更适合。当然,如果你一定要跟我杠现代主机的内存对于这点消耗根本不算什么,那我也办法。
总之,低并发、一般的业务场景下AtomicLong是足够了。如果并发量很多,存在大量写多读少的情况,那LongAdder可能更合适。适合的才是最好的,如果真出现了需要考虑到底用AtomicLong好还是LongAdder的业务场景,那么这样的讨论是没有意义的,因为这种情况下要么进行性能测试,以准确评估在当前业务场景下两者的性能,要么换个思路寻求其它解决方案。
最后,给出国外一位博主对LongAdder和AtomicLong的性能评测,以供参考:http://blog.palominolabs.com/2014/02/10/java-8-performance-improvements-longadder-vs-atomiclong
二、LongAdder原理
之前说了, AtomicLong 是多个线程针对单个热点值value进行原子操作。而 LongAdder 是每个线程拥有自己的槽,各个线程一般只对自己槽中的那个值进行CAS操作。
比如有三个ThreadA、ThreadB、ThreadC,每个线程对value增加10。
对于 AtomicLong ,最终结果的计算始终是下面这个形式:
$$
value = 10 + 10 + 10 = 30
$$
但是对于 LongAdder 来说,内部有一个base
变量,一个Cell[]
数组。
base
变量:非竞态条件下,直接累加到该变量上
Cell[]
数组:竞态条件下,累加个各个线程自己的槽Cell[i]
中
最终结果的计算是下面这个形式:
$$
value = base + \sum_{i=0}^nCell[i]
$$
2.1 LongAdder的内部结构
LongAdder 只有一个空构造器,其本身也没有什么特殊的地方,所有复杂的逻辑都在它的父类 Striped64 中。
来看下 Striped64 的内部结构,这个类实现一些核心操作,处理64位数据。
Striped64 只有一个空构造器,初始化时,通过Unsafe获取到类字段的偏移量,以便后续CAS操作:
上面有个比较特殊的字段是threadLocalRandomProbe
,可以把它看成是线程的hash值。这个后面我们会讲到。
定义了一个内部Cell类,这就是我们之前所说的槽,每个Cell对象存有一个value值,可以通过 Unsafe 来CAS操作它的值:
其它的字段:
可以看到 Cell[] 就是之前提到的槽数组, base 就是非并发条件下的基数累计值。
2.2 LongAdder的核心方法
还是通过例子来看:
假设现在有一个 LongAdder 对象la,四个线程A、B、C、D同时对la进行累加操作。
LongAdder la = new LongAdder();
la.add(10);
① ThreadA调用add方法(假设此时没有并发):
初始时Cell[]为null,base为0。所以ThreadA会调用 casBase 方法(定义在 Striped64 中),因为没有并发,CAS操作成功将base变为10:
可以看到,如果线程A、B、C、D线性执行,那 casBase 永远不会失败,也就永远不会进入到 base 方法的if块中,所有的值都会累积到 base 中。
那么,如果任意线程有并发冲突,导致 caseBase 失败呢?
失败就会进入if方法体:
这个方法体会先再次判断 Cell[] 槽数组有没初始化过,如果初始化过了,以后所有的CAS操作都只针对槽中的Cell;否则,进入 longAccumulate 方法。
整个 add 方法的逻辑如下图:
可以看到,只有从未出现过并发冲突的时候,base基数才会使用到,一旦出现了并发冲突,之后所有的操作都只针对
Cell[]
数组中的单元Cell。
如果Cell[]
数组未初始化,会调用父类的longAccumelate
去初始化Cell[]
,如果Cell[]
已经初始化但是冲突发生在Cell
单元内,则也调用父类的longAccumelate
,此时可能就需要对Cell[]
扩容了。
这也是LongAdder设计的精妙之处:尽量减少热点冲突,不到最后万不得已,尽量将CAS操作延迟。
2.3 Striped64的核心方法
我们来看下 Striped64 的核心方法 longAccumulate 到底做了什么:
上述代码首先给当前线程分配一个hash值,然后进入一个自旋,这个自旋分为三个分支:
- CASE1:Cell[]数组已经初始化
- CASE2:Cell[]数组未初始化
- CASE3:Cell[]数组正在初始化中
CASE2:Cell[]数组未初始化
我们之前讨论了,初始时 Cell[] 数组还没有初始化,所以会进入分支②:
首先会将 cellsBusy 置为 1-加锁状态
然后,初始化 Cell[] 数组(初始大小为2),根据当前线程的hash值计算映射的索引,并创建对应的 Cell 对象, Cell 单元中的初始值x就是本次要累加的值。
CASE3:Cell[]数组正在初始化中
如果在初始化过程中,另一个线程ThreadB也进入了 longAccumulate 方法,就会进入分支③:
可以看到,分支③直接操作 base 基数,将值累加到 base 上。
CASE1:Cell[]数组已经初始化
如果初始化完成后,其它线程也进入了 longAccumulate 方法,就会进入分支①:
整个 longAccumulate 的流程图如下:
2.4 LongAdder的sum方法
最后,我们来看下 LongAdder 的 sum 方法:
sum 求和的公式就是我们开头说的:
$$
value = base + \sum_{i=0}^nCell[i]
$$
需要注意的是,这个方法只能得到某个时刻的近似值,这也就是 LongAdder 并不能完全替代 LongAtomic 的原因之一。
三、LongAdder的其它兄弟
JDK1.8时,java.util.concurrent.atomic
包中,除了新引入 LongAdder 外,还有引入了它的三个兄弟类: LongAccumulator 、 DoubleAdder 、 DoubleAccumulator
3.1 LongAccumulator
LongAccumulator 是 LongAdder 的增强版。 LongAdder 只能针对数值的进行加减运算,而 LongAccumulator 提供了自定义的函数操作。其构造函数如下:
通过 LongBinaryOperator ,可以自定义对入参的任意操作,并返回结果( LongBinaryOperator 接收2个long作为参数,并返回1个long)
LongAccumulator 内部原理和LongAdder几乎完全一样,都是利用了父类 Striped64 的 longAccumulate 方法。这里就不再赘述了,读者可以自己阅读源码。
3.2 DoubleAdder和DoubleAccumulator
从名字也可以看出, DoubleAdder 和 DoubleAccumulator 用于操作double原始类型。
与 LongAdder 的唯一区别就是,其内部会通过一些方法,将原始的double类型,转换为long类型,其余和 LongAdder 完全一样:
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。