我们在之前的文章中介绍到了 AtomicLong ,如果你还不了解,我建议你阅读一下这篇文章
为什么我要先说 AtomicLong 呢?因为 LongAdder 的设计是根据 AtomicLong 的缺陷来设计的。
为什么引入 LongAdder
我们知道,AtomicLong 是利用底层的 CAS 操作来提供并发性的,比如 addAndGet
方法
public final long addAndGet(long delta) { return unsafe.getAndAddLong(this, valueOffset, delta) + delta; }
我们还知道,CAS 是一种轻量级的自旋方法,它的逻辑是采用自旋的方式不断更新目标值,直到更新成功。(也即乐观锁的实现模式)
在并发数量比较低的场景中,线程冲突的概率比较小,自旋的次数不会很多。但是,在并发数量激增的情况下,会出现大量失败并不断自旋的场景,此时 AtomicLong 的自旋次数很容易会成为瓶颈。
为了解决这种缺陷,引入了本篇文章的主角 --- LongAdder,它主要解决高并发环境下 AtomictLong 的自旋瓶颈问题。
认识 LongAdder
我们先看一下 JDK 源码中关于 LongAdder 的讨论
这段话很清晰的表明,在多线程环境下,如果业务场景更侧重于统计数据
或者收集信息
的话,LongAdder 要比 AtomicLong 有更好的性能,吞吐量更高,但是会占用更多的内存。在并发数量较低或者单线程场景中,AtomicLong 和 LongAdder 具有相同的特征,也就是说,在上述场景中,AtomicLong 和 LongAdder 可以互换。
首先我们先来看一下 LongAdder类的定义:
可以看到,LongAdder 继承于 Striped64类,并实现了 Serializable 接口。
Striped64 又是继承于 Number 类,现在你可能不太清楚 Striped64 是做什么的,但是你别急。
我们知道,Number 类是基本数据类型的包装类以及原子性包装类的父类,继承 Number 类表示它可以当作数值进行处理,然而这样并不能说明什么,它对我们来说还是一头雾水,随后我翻了翻 Striped64 的继承体系,发现这个类有几个实现,而这几个实现能够很好的帮助我们理解什么是 Striped64。
在其中我们看到了 Accumulator,它的中文概念就是累加器,所以我们猜测 Striped64 也能实现累加的功能。果不其然,在我求助了部分帖子之后,我们可以得出来一个结论:Striped64 就是一个支持 64 位的累加器。
但是那两个 Accumulator 的实现可以实现累加,这个好理解,但是这两个 Adder 呢?它们也能实现累加的功能吗?
我们再次细致的查看一下 LongAdder 和 DoubleAdder 的源码(因为这两个类非常相似)。
(下面的叙事风格主要以 LongAdder 为准,DoubleAdder 会兼带着聊一下,但不是主要叙事对象,毕竟这两个类的基类非常相似。)
在翻阅一波源码过后,我们发现了一些方法,比如 increment、decrement、add、reset、sum等,这些方法看起来好像是一个累加器所拥有的功能。
在详细翻阅源码后,我认证了我的想法,因为 increment 、decrement 都是调用的 add 方法,而 add 方法底层则使用了 longAccmulate,我给你画个图你就知道了。
所以,LongAdder 的第一个特性就是可以处理数值,而实现了 Serializable 这个接口表示 LongAdder 可以序列化,以便存储在文件中或在网络上传输。
然后我们继续往下走,现在时间的罗盘指向了 LongAdder 的 add 方法,我们脑子里面已经有印象,这个 LongAdder 方法是来做并发累加的,所以这个 add 方法如果不出意外就是并发的累加方法,那么很奇怪了,这个方法为什么没有使用 synchronized 呢?
我们继续观察 add 方法都做了哪些操作。
开始时就是分配了一些变量而已啊,这也没什么特殊的,继续看到第二行的时候,我们发现有一个 cells 对象,这个 cells 对象是个数组,好像是个全局的。。。。。。那么在哪里定义的呢?
一想到还有个 Striped64 这个类,果断点进去看到了这个全局变量,另外,Striped64 这个类很重要,我们可不能把它忘了。
在 Striped64 类中,赫然出现了下面这三个重要的全局变量。
由 transient 修饰的这三个全局变量,会保证不会被序列化。
我们先不解释这三个变量是什么,有什么用,因为我们刚刚撸到了 add 方法,所以还是回到 add 方法上来:
继续向下走,我们看到了 caseBase 方法,这个方法又是干什么的?点进去,发现还是 Striped64 中的方法,现在,我们陷入了沉思。
我们上面只是知道了 Striped64 是一个累加器,但是我现在要不要撸一遍 Striped64 的源码??????
认识 Striped64
果然出来混都是要还的,上面没有介绍的 Striped64 的三个变量,现在就要好好介绍一波了。
- cells 它是一个数组,如果不是 null ,它的大小就是 2 的次方。
- base 是 Striped64 的基本数值,主要在没有争用时使用,同时也作为 cells 初始化时使用,使用 CAS 进行更新。
- cellsBusy 它是一个
SpinLock(自旋锁)
,在初始化 cells 时使用。
除了这三个变量之外,还有一个变量,我竟然把它忽视了,罪过罪过。
它表示的是 CPU 的核数。
好像并没有看出来什么玄机,接着往下走
这里出现了一个 Cell 元素的内部类,里面出现了 long 型的 value 值,cas 方法,UNSAFE,valueOffSet 元素,如果你看过我的这篇文章 Atomic 原子工具类详解 或者你了解 AtomicLong 的设计的话,你就知道,Cell 的设计是和 AtomicLong 完全一样的,都是使用了 volatile 变量、Unsafe 加上字段的偏移量,再用 CAS 进行修改。
而这个 Cell 元素,就是 cells 数组中的每个对象。
这里比较特殊的是 @sun.misc.Contended
注解,它是 Java 8 中新增的注解,用来避免缓存的伪共享,减少 CPU 缓存级别的竞争。
害,就这?
先不要着急,Striped64 最核心的功能是分别为 LongAdder 和 DoubleAdder 提供并发累加的功能,所以 Striped64 中的 longAccumulate 和 doubleAccumulate 才是关键,我们主要介绍 longAccumulate 方法,方法比较长,我们慢慢进入节奏。