而在 JDK 8 里面 AtomicLong 里面的一些方法也是自旋,但是就不仅仅依赖于
cmpxchg 指令做了,比如还是上面这个方法:
可以看到这里面还是有一个 do-while 的循环,还是调用的 compareAndSwapLong 方法:
这个方法对应的 Lock 前缀指令是我们前面提到过的 xadd 指令。
从 Java 代码的角度来看,都是自旋,都是 compareAndSwapLong 方法。没有什么差异。
但是从这篇 oracle 官网的文章,我们可以窥见 JDK 8 在 x86 平台上对 compareAndSwapLong 方法做了一些操作,使用了 xadd 汇编指令代替 CAS 操作。
xadd 指令是 fetch and add。
cmpxchg 指令是 compare and swap。
xadd 指令的性能是优于 cmpxchg 指令的。
具体可以看看这篇 oracle 官网的文章:
https://blogs.oracle.com/dave/atomic-fetch-and-add-vs-compare-and-swap
文章下面的评论,可以多注意一下,我截取其中两个,大家品一品:
然后是这个:
总之就是:这篇文章说的有道理,我们(Dave and Doug)也在思考这个问题。所以我们会在 JIT 上面搞事情,在 x86 平台上把 CAS 操作替换为 LOCK:XADD 指令。
(这个地方我之前理解的有问题,经过朋友的指正后才修改过来。)
所以,JDK 8 之后的 AtomicLong 里面的方法都是经过改良后, xadd+cmpxchg 双重加持的方法。
另外需要注意的是,我怕有的朋友懵逼,专门多提一嘴:CAS 是指一次比较并交换的过程,成功了就返回 true,失败了则返回 false,强调的是一次。而自旋 CAS 是在死循环里面进行比较并交换,只要不返回 true 就一直循环。
所以,不要一提到 CAS 就说循环时间开销大。前面记得加上“自旋”和“竞争大”两个条件。
至于 JDK 8 使用 xadd 汇编指令代替 CAS 操作的是否真的是性能更好了,可以看看这篇 oracle 官网的文章:
https://blogs.oracle.com/dave/atomic-fetch-and-add-vs-compare-and-swap
文章下面的评论,可以多注意一下,我截取其中一个,大家品一品:
经过我们前面的分析,AtomicLong 从 JDK 7 到 JDK 8 是有一定程度上的性能优化的,但是改动并不大。
还是存在一个问题:虽然它可以实现原子性的增减操作,但是当竞争非常大的时候,被操作的这个 value 就是一个热点数据,所有线程都要去对其进行争抢,导致并发修改时冲突很大。
所以,归根到底它的主要问题还是出在共享热点数据上。
为了解决这个问题,Doug Lea 在 JDK 8 里面引入了 LongAdder 类。
更加牛逼的LongAdder
大家先看一下官网上的介绍:
上面的截图一共两段话,是对 LongAdder 的简介,我给大家翻译并解读一下。
首先第一段:当有多线程竞争的情况下,有个叫做变量集合(set of variables)的东西会动态的增加,以减少竞争。sum() 方法返回的是某个时刻的这些变量的总和。
所以,我们知道了它的返回值,不论是 sum() 方法还是 longValue() 方法,都是那个时刻的,不是一个准确的值。
意思就是你拿到这个值的那一刻,这个值其实已经变了。
这点是非常重要的,为什么会是这样呢?
我们对比一下 AtomicLong 和 LongAdder 的自增方法就可以知道了:
AtomicLong 的自增是有返回值的,就是一个这次调用之后的准确的值,这是一个原子性的操作。
LongAdder 的自增是没有返回值的,你要获取当前值的时候,只能调用 sum 方法。
你想这个操作:先自增,再获取值,这就不是原子操作了。
所以,当多线程并发调用的时候,sum 方法返回的值必定不是一个准确的值。除非你加锁。
该方法上的说明也是这样的:
至于为什么不能返回一个准确的值,这就是和它的设计相关了,这点放在后面去说。
然后第二段:当在多线程的情况下对一个共享数据进行更新(写)操作,比如实现一些统计信息类的需求,LongAdder 的表现比它的老大哥 AtomicLong 表现的更好。在并发不高的时候,两个类都差不多。但是高并发时 ngAdder 的吞吐量明显高一点,它也占用更多的空间。这是一种空间换时间的思想。
这段话其实是接着第一段话在进行描述的。
因为它在多线程并发情况下,没有一个准确的返回值,所以当你需要根据返回值去搞事情的时候,你就要仔细思考思考,这个返回值你是要精准的,还是大概的统计类的数据就行。
比如说,如果你是用来做序号生成器,所以你需要一个准确的返回值,那么还是用 AtomicLong 更加合适。
如果你是用来做计数器,这种写多读少的场景。比如接口访问次数的统计类需求,不需要时时刻刻的返回一个准确的值,那就上 LongAdder 吧。
总之,AtomicLong 是可以保证每次都有准确值,而 LongAdder 是可以保证最终数据是准确的。高并发的场景下 LongAdder 的写性能比 AtomicLong 高。
接下来探讨三个问题:
- LongAdder 是怎么解决多线程操作热点 value 导致并发修改冲突很大这个问题的?
- 为什么高并发场景下 LongAdder 的 sum 方法不能返回一个准确的值?
- 为什么高并发场景下 LongAdder 的写性能比 AtomicLong 高?
先带大家看个图片,看不懂没有关系,先有个大概的印象:
接下来我们就去探索源码,源码之下无秘密。
从源码我们可以看到 add 方法是关键:
里面有 cells 、base 这样的变量,所以在解释 add 方法之前,我们先看一下 这几个成员变量。
这几个变量是 Striped64 里面的。
LongAdder 是 Striped64 的子类:
其中的四个变量如下:
- NCPU:cpu 的个数,用来决定 cells 数组的大小。
- cells:一个数组,当不为 null 的时候大小是 2 的次幂。里面放的是 cell 对象。
- base : 基数值,当没有竞争的时候直接把值累加到 base 里面。还有一个作用就是在 cells 初始化时,由于 cells 只能初始化一次,所以其他竞争初始化操作失败线程会把值累加到 base 里面。
- cellsBusy:当 cells 在扩容或者初始化的时候的锁标识。
之前,文档里面说的 set of variables 就是这里的 cells。