《Java并发编程之美》第 4 章 Java 并发包中原子操作类原理剖析 阅读笔记 7
第 4 章 Java 并发包中原子操作类原理剖析
JUC 包提供了一系列的原子性操作类,这些类都是使用非阻塞算法 CAS 实现的,相比使用锁实现原子性操作这在性能上有很大提高。
由于原子性操作类的原理都大致相同,所以本章只讲解最简单的 AtomicLong 类的实现原理以及 JDK 8 中新增的 LongAdder 和 LongAccumulator 类的原理。
有了这些基础,再去理解其他原子性操作类的实现就不会感到困难了。
4.1 原子变量操作类
JUC 并发包中包含有 AtomicInteger、AtomicLong 和 AtomicBoolean 等原子性操作类,它们的原理类似,
本章讲解 AtomicLong 类。AtomicLong 是原子性递增或者递减类,其内部使用 Unsafe 来实现,我们看下面的代码。
java
public class AtomicLong extends Number implements java.io.Serializable {
private static final long serialVersionUID = 1927816293512124184L;
//(1)获取Unsafe实例
private static final Unsafe unsafe = Unsafe.getUnsafe();
//(2)存放变量value的偏移量
private static final long valueOffset;
//(3)判断JVM是否支持Long类型无锁CAS
static final boolean VM_SUPPORTS_LONG_CAS = VMSupportsCS8();
private static native boolean VMSupportsCS8();
static {
try {
//(4)获取value在AtomicLong中的偏移量
valueOffset = unsafe.objectFieldOffset
(AtomicLong.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//(5)实际变量值
private volatile long value;
public AtomicLong(long initialValue) {
value = initialValue;
}
....
}
代码(1)通过 Unsafe.getUnsafe() 方法获取到 Unsafe 类的实例,这里你可能会有疑问,
为何能通过 Unsafe.getUnsafe() 方法获取到 Unsafe 类的实例?其实这是因为 AtomicLong 类也是在 rt.jar 包下面的,
AtomicLong 类就是通过 BootStarp 类加载器进行加载的。
代码(5)中的 value 被声明为 volatile 的,这是为了在多线程下保证内存可见性,value 是具体存放计数的变量。
代码(2)(4)获取 value 变量在 AtomicLong 类中的偏移量。
下面重点看下 AtomicLong 中的主要函数。
1.递增和递减操作代码
java
//(6)调用unsafe方法,原子性设置value值为原始值+1,返回值为递增后的值
public final long incrementAndGet() {
return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
}
//(7)调用unsafe方法,原子性设置value值为原始值-1,返回值为递减之后的值
public final long decrementAndGet() {
return unsafe.getAndAddLong(this, valueOffset, -1L) -1L;
}
//(8)调用unsafe方法,原子性设置value值为原始值+1,返回值为原始值
public final long getAndIncrement() {
return unsafe.getAndAddLong(this, valueOffset, 1L);
}
//(9)调用unsafe方法,原子性设置value值为原始值-1,返回值为原始值
public final long getAndDecrement() {
return unsafe.getAndAddLong(this, valueOffset, -1L);
}
在如上代码内部都是通过调用 Unsafe 的 getAndAddLong 方法来实现操作 ,这个函数是个原子性操作,
这里第一个参数是 AtomicLong 实例的引用,第二个参数是 value 变量在 AtomicLong 中的偏移值,第三个参数是要设置的第二个变量的值。
其中,getAndIncrement 方法在 JDK 7 中的实现逻辑为
java
public final long getAndIncrement() {
while (true) {
long current = get();
long next = current + 1;
if (compareAndSet(current, next))
return current;
}
}
在如上代码中,每个线程是先拿到变量的当前值(由于 value 是 volatile 变量,所以这里拿到的是最新的值),
然后在工作内存中对其进行增 1 操作,而后使用 CAS 修改变量的值。如果设置失败,则循环继续尝试,直到设置成功。
而 JDK 8 中的逻辑为
java
public final long getAndIncrement() {
return unsafe.getAndAddLong(this, valueOffset, 1L);
}
其中 JDK 8 中 unsafe.getAndAddLong 的代码为
java
public final long getAndAddLong(Object paramObject, long paramLong1, long paramLong2)
{
long l;
do
{
l = getLongvolatile(paramObject, paramLong1);
} while (! compareAndSwapLong(paramObject, paramLong1, l, l + paramLong2));
return l;
}
可以看到,JDK 7 的 AtomicLong 中的循环逻辑已经被 JDK 8 中的原子操作类 UNsafe 内置了,之所以内置应该是考虑到这个函数在其他地方也会用到,而内置可以提高复用性。
2.boolean compareAndSet(long expect, long update) 方法
由如上代码可知,在内部还是调用了 unsafe.compareAndSwapLong 方法。如果原子变量中的 value 值等于 expect,则使用 update 值更新该值并返回 true,否则返回 false。
下面通过一个多线程使用 AtomicLong 统计 0 的个数的例子来加深对 AtomicLong 的理解。
java
package com.yangjunbo.juc.four;
import java.util.concurrent.atomic.AtomicLong;
/**
统计0的个数
*/
public class Atomic
{
//(10)创建Long型原子计数器
private static AtomicLong atomicLong = new AtomicLong();
//(11)创建数据源
private static Integer[] arrayOne = new Integer[]{0,1,2,3,0,5,6,0,56,0};
private static Integer[] arrayTwo = new Integer[]{10,1,2,3,0,5,6,0,56,0};
public static void main( String[] args ) throws InterruptedException
{
//(12)线程one统计数组arrayOne中0的个数
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
int size = arrayOne.length;
for(int i=0; i<size; ++i){
if(arrayOne[i].intValue() == 0){
atomicLong.incrementAndGet();
}
}
}
});
//(13)线程two统计数组arrayTwo中0的个数
Thread threadTwo = new Thread(new Runnable() {
@Override
public void run() {
int size = arrayTwo.length;
for(int i=0; i<size; ++i){
if(arrayTwo[i].intValue() == 0){
atomicLong.incrementAndGet();
}
}
}
});
//(14)启动子线程
threadOne.start();
threadTwo.start();
//(15)等待线程执行完毕
threadOne.join();
threadTwo.join();
System.out.println("count 0:" + atomicLong.get());
}
}
输出结果为

如上代码中的两个线程各自统计自己所持数据中 0 的个数,每当找到一个 0 就会调用 AtomicLong 的原子性递增方法。
在没有原子类的情况下,实现计数器需要使用一定的同步措施,比如使用 synchronized 关键字等,但是这些都是阻塞算法,对性能有一定损耗,而本章介绍的这些原子操作类都使用 CAS 非阻塞算法,性能更好。
但是在高并发情况下 AtomicLong 还会存在性能问题。(更新失败的线程会循环尝试,直到更新成功)
JDK 8 提供了一个在高并发下性能更好的 LongAdder 类,下面我们来讲解这个类。
4.2 JDK 8 新增的原子操作类 LongAdder
4.2.1 LongAdder 简单介绍
前面讲过,AtomicLong 通过 CAS 提供了非阻塞的原子性操作,相比使用阻塞算法的同步器来说它的性能已经很好了,
但是 JDK 开发组并不满足于此。使用 AtomicLong 时,在高并发下大量线程会同时去竞争更新同一个原子变量,
但是由于同时只有一个线程的 CAS 操作会成功,这就造成了大量线程竞争失败后,会通过无限循环不断进行自旋尝试 CAS 的操作,
而这会白白浪费 CPU 资源。
因此 JDK 8 新增了一个原子性递增或者递减类 LongAdder 用来克服在高并发下使用 AtomicLong 的缺点。
既然 AtomicLong 的性能瓶颈是由于过多线程同时去竞争一个变量的更新而产生的,
那么如果把一个变量分解为多个变量,让同样多的线程去竞争多个资源,是不是就解决了性能问题?是的,
LongAdder 就是这个思路。下面通过图来理解两者设计的不同之处,如图 4-1 所示。
图 4-1

如图 4-1 所示,使用 AtomicLong 时,是多个线程同时竞争同一个原子变量。
图 4-2

如图 4-2 所示,使用 LongAdder 时,则是在内部维护多个 Cell 变量,每个 Cell 里面有一个初始值为 0 的 long 型变量,
这样,在同等并发量的情况下,争夺单个变量更新操作的线程量会减少,这变相地减少了争夺共享资源的并发量。
另外,多个线程在争夺同一个 Cell 原子变量时如果失败了,它并不是在当前 Cell 变量上一直自旋 CAS 重试,而是尝试在其他 Cell 的变量上进行 CAS 尝试,这个改变增加了当前线程重试 CAS 成功的可能性。
最后,在获取 LongAdder 当前值时,是把所有 Cell 变量的 value 值累加后再加上 base 返回的。
LongAdder 维护了一个延迟初始化的原子性更新数组(默认情况下 Cell 数组是 null)和一个基值变量 base。
由于 Cells 占用的内存是相对比较大的,所以一开始并不创建它,而是在需要时创建,也就是惰性加载。
当一开始判断 Cell 数组是null并且并发线程较少时,所有的累加操作都是对 base 变量进行的。
保持 Cell 数组的大小为 2 的 N 次方,在初始化时 Cell 数组中的 Cell 元素个数为 2,数组里面的变量实体是 Cell 类型。
Cell 类型是 AtomicLong 的一个改进,用来减少缓存的争用,也就是解决伪共享问题。
对于大多数孤立的多个原子操作进行字节填充是浪费的,因为原子性操作都是无规律地分散在内存中的(也就是说多个原子性变量的内存地址是不连续的),多个原子变量被放入同一个缓存行的可能性很小。
但是原子性数组元素的内存地址是连续的,所以数组内的多个元素能经常共享缓存行,
因此这里使用 @sun.misc.Contended 注解对 Cell 类进行字节填充,这防止了数组中多个元素共享一个缓存行,在性能上是一个提升。
4.2.2 LongAdder 代码分析
为了解决高并发下多线程对一个变量 CAS 争夺失败后进行自旋而造成的降低并发性能问题 ,
LongAdder 在内部维护多个 Cell 元素(一个动态的 Cell 数组)来分担对单个变量进行争夺的开销。
下面围绕以下话题从源码角度来分析 LongAdder 的实现:
(1)LongAdder的结构是怎样的?
(2)当前线程应该访问Cell数组里面的哪一个Cell元素?
(3)如何初始化Cell数组?
(4)Cell数组如何扩容?
(5)线程访问分配的Cell元素有冲突后如何处理?
(6)如何保证线程操作被分配的Cell元素的原子性?
首先看下 LongAdder 的类图结构,如图 4-3 所示。
图 4-3

由该图可知,LongAdder 类继承自 Striped64 类,在 Striped64 内部维护着三个变量。
LongAdder 的真实值其实是 base 的值与 Cell 数组里面所有 Cell 元素中的 value 值的累加,
base 是个基础值,默认为 0。
cellsBusy 用来实现自旋锁,状态值只有 0 和 1,当创建 Cell 元素,扩容 Cell 数组或者初始化 Cell 数组时,使用 CAS 操作该变量来保证同时只有一个线程可以进行其中之一的操作。
下面看 Cell 的构造。

可以看到,Cell 的构造很简单,其内部维护一个被声明为 volatile 的变量,
这里声明为 volatile 是因为线程操作 value 变量时没有使用锁,为了保证变量的内存可见性这里将其声明为 volatile 的。
另外 cas 函数通过 CAS 操作,保证了当前线程更新时被分配的 Cell 元素中 value 值的原子性。
另外,Cell 类使用 @sun.misc.Contended 修饰是为了避免伪共享。到这里我们回答了问题 1 和问题 6。
long sum() 返回当前的值,内部操作是累加所有 Cell 内部的 value 值后再累加 base。
例如下面的代码,由于计算总和时没有对 Cell 数组进行加锁,所以在累加过程中可能有其他线程对 Cell 中的值进行了修改,
也有可能对数组进行了扩容,所以 sum 返回的值并不是非常精确的,其返回值并不是一个调用 sum 方法时的原子快照值。

void reset() 为重置操作,如下代码把 base 置为 0,如果 Cell 数组有元素,则元素值被重置为 0。

long sumThenReset() 是 sum 的改造版本,如下代码在使用 sum 累加对应的 Cell 值后,把当前 Cell 的值重置为 0, base 重置为 0。
这样,当多线程调用该方法时会有问题,比如考虑第一个调用线程清空 Cell 的值,则后一个线程调用时累加的都是 0 值。

long longValue() 等价于 sum()。

下面主要看下 add 方法的实现,从这个方法里面就可以找到其他问题的答案。
java
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
if ((as = cells) ! = null || ! casBase(b = base, b + x)) {//(1)
boolean uncontended = true;
if (as == null || (m = as.length -1) < 0 ||//(2)
(a = as[getProbe() & m]) == null ||//(3)
!(uncontended = a.cas(v = a.value, v + x)))//(4)
longAccumulate(x, null, uncontended); //(5)
}
}
final boolean casBase(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
}
代码(1)首先看 cells 是否为 null,如果为 null 则当前在基础变量 base 上进行累加,这时候就类似 AtomicLong 的操作。
如果 cells 不为 null 或者线程执行代码(1)的 CAS 操作失败了,则会去执行代码(2)。
代码(2)(3)决定当前线程应该访问 cells 数组里面的哪一个 Cell 元素,
如果当前线程映射的元素存在则执行代码(4),使用 CAS 操作去更新分配的 Cell 元素的 value 值,
如果当前线程映射的元素不存在或者存在但是 CAS 操作失败则执行代码(5)。
其实将代码(2)(3)(4)合起来看就是获取当前线程应该访问的 cells 数组的 Cell 元素,然后进行 CAS 更新操作,
只是在获取期间如果有些条件不满足则会跳转到代码(5)执行。
另外当前线程应该访问 cells 数组的哪一个 Cell 元素是通过 getProbe()& m 进行计算的,
其中 m 是当前 cells 数组元素个数 -1, getProbe() 则用于获取当前线程中变量 threadLocalRandomProbe 的值,这个值一开始为 0,在代码(5)里面会对其进行初始化。
并且当前线程通过分配的 Cell 元素的 cas 函数来保证对 Cell 元素 value 值更新的原子性,到这里我们回答了问题 2 和问题 6。
下面重点研究 longAccumulate 的代码逻辑,这是 cells 数组被初始化和扩容的地方。
java
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
//(6) 初始化当前线程的变量threadLocalRandomProbe的值
int h;
if ((h = getProbe()) == 0) {
ThreadLocalRandom.current(); //
h = getProbe();
wasUncontended = true;
}
boolean collide = false;
for (; ; ) {
Cell[] as; Cell a; int n; long v;
if ((as = cells) ! = null && (n = as.length) > 0) {//(7)
if ((a = as[(n -1) & h]) == null) {//(8)
if (cellsBusy == 0) { // Try to attach new Cell
Cell r = new Cell(x); // Optimistically create
if (cellsBusy == 0 && casCellsBusy()) {
boolean created = false;
try { // Recheck under lock
Cell[] rs; int m, j;
if ((rs = cells) ! = null &&
(m = rs.length) > 0 &&
rs[j = (m -1) & h] == null) {
rs[j] = r;
created = true;
}
} finally {
cellsBusy = 0;
}
if (created)
break;
continue; // Slot is now non-empty
}
}
collide = false;
}
else if (! wasUncontended) // CAS already known to fail
wasUncontended = true;
//当前Cell存在,则执行CAS设置(9)
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
//当前Cell数组元素个数大于CPU个数(10)
else if (n >= NCPU || cells ! = as)
collide = false; // At max size or stale
//是否有冲突(11)
else if (! collide)
collide = true;
//如果当前元素个数没有达到CPU个数并且有冲突则扩容(12)
else if (cellsBusy == 0 && casCellsBusy()) {
try {
if (cells == as) { // Expand table unless stale
//12.1
Cell[] rs = new Cell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
cells = rs;
}
} finally {
//12.2
cellsBusy = 0;
}
//12.3
collide = false;
continue; // Retry with expanded table
}
//(13)为了能够找到一个空闲的Cell,重新计算hash值,xorshift算法生成随机数
h = advanceProbe(h);
}
//初始化Cell数组(14)
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
boolean init = false;
try {
if (cells == as) {
//14.1
Cell[] rs = new Cell[2];
//14.2
rs[h & 1] = new Cell(x);
cells = rs;
init = true;
}
} finally {
//14.3
cellsBusy = 0;
}
if (init)
break;
}
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break; // Fall back on using base
}
}
上面代码比较复杂,这里我们主要关注问题 3、问题 4 和问题 5。
当每个线程第一次执行到代码(6)时,会初始化当前线程变量 threadLocalRandomProbe 的值,上面也说了,这个变量在计算当前线程应该被分配到 cells 数组的哪一个 Cell 元素时会用到。
cells 数组的初始化是在代码(14)中进行的,其中 cellsBusy 是一个标识,
为 0 说明当前 cells 数组没有在被初始化或者扩容,也没有在新建 Cell 元素,
为 1 则说明 cells 数组在被初始化或者扩容,或者当前在创建新的 Cell 元素、通过 CAS 操作来进行 0 或 1 状态的切换,这里使用 casCellsBusy函数。
假设当前线程通过 CAS 设置 cellsBusy 为 1,则当前线程开始初始化操作,那么这时候其他线程就不能进行扩容了。
如代码(14.1)初始化 cells 数组元素个数为 2,然后使用 h&1 计算当前线程应该访问 cells 数组的哪个位置,
也就是使用当前线程的 threadLocalRandomProbe 变量值 &(cells 数组元素个数 -1),然后标识 cells 数组已经被初始化,
最后代码(14.3)重置了 cellsBusy 标记。显然这里没有使用 CAS 操作,却是线程安全的,原因是 cellsBusy 是 volatile 类型的,这保证了变量的内存可见性,另外此时其他地方的代码没有机会修改 cellsBusy 的值。
在这里初始化的 cells 数组里面的两个元素的值目前还是 null。这里回答了问题 3,知道了 cells 数组如何被初始化。
cells 数组的扩容是在代码(12)中进行的,对 cells 扩容是有条件的,也就是代码(10)(11)的条件都不满足的时候。
具体就是当前 cells 的元素个数小于当前机器 CPU 个数并且当前多个线程访问了 cells 中同一个元素,从而导致冲突使其中一个线程 CAS 失败时才会进行扩容操作。
这里为何要涉及 CPU 个数呢?其实在基础篇中已经讲过,只有当每个 CPU 都运行一个线程时才会使多线程的效果最佳,
也就是当 cells 数组元素个数与 CPU 个数一致时,每个 Cell 都使用一个 CPU 进行处理,这时性能才是最佳的。
代码(12)中的扩容操作也是先通过 CAS 设置 cellsBusy 为 1,然后才能进行扩容。
假设 CAS 成功则执行代码(12.1)将容量扩充为之前的 2 倍,并复制 Cell 元素到扩容后数组。
另外,扩容后 cells 数组里面除了包含复制过来的元素外,还包含其他新元素,这些元素的值目前还是 null。这里回答了问题 4。
在代码(7)(8)中,当前线程调用 add 方法并根据当前线程的随机数 threadLocalRandomProbe 和 cells 元素个数计算要访问的 Cell 元素下标,然后如果发现对应下标元素的值为 null,则新增一个 Cell 元素到 cells 数组,并且在将其添加到 cells 数组之前要竞争设置 cellsBusy 为 1。
代码(13)对 CAS 失败的线程重新计算当前线程的随机值 threadLocalRandomProbe,以减少下次访问 cells 元素时的冲突机会。这里回答了问题 5。
4.2.3 小结
本节介绍了 JDK 8 中新增的 LongAdder 原子性操作类,该类通过内部 cells 数组分担了高并发下多线程同时对一个原子变量进行更新时的竞争量,让多个线程可以同时对 cells 数组里面的元素进行并行的更新操作。
另外,数组元素 Cell 使用 @sun.misc.Contended 注解进行修饰,这避免了 cells 数组内多个原子变量被放入同一个缓存行,也就是避免了伪共享,这对性能也是一个提升。
4.3 LongAccumulator 类原理探究
LongAdder 类是 LongAccumulator 的一个特例,LongAccumulator 比 LongAdder 的功能更强大。
例如下面的构造函数,其中 accumulatorFunction 是一个双目运算器接口,其根据输入的两个参数返回一个计算值,
identity 则是 LongAccumulator 累加器的初始值。


上面提到,LongAdder 其实是 LongAccumulator 的一个特例,调用 LongAdder 就相当于使用下面的方式调用 LongAccumulator:
java
LongAdder adder = new LongAdder();
java
LongAccumulator accumulator = new LongAccumulator(new LongBinaryOperator() {
@Override
public long applyAsLong(long left, long right) {
return left + right;
}
}, 0);
LongAccumulator 相比于 LongAdder,可以为累加器提供非 0 的初始值,后者只能提供默认的 0 值。
另外,前者还可以指定累加规则,比如不进行累加而进行相乘,只需要在构造 LongAccumulator 时传入自定义的双目运算器即可,后者则内置累加的规则。
从下面代码我们可以知道,LongAccumulator 相比于 LongAdder 的不同在于,在调用 casBase 时后者传递的是 b+x,
前者则使用了 r = function.applyAsLong(b = base, x) 来计算。
java
//LongAdder的add
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
if ((as = cells) ! = null || ! casBase(b = base, b + x)) {
boolean uncontended = true;
if (as == null || (m = as.length -1) < 0 ||
(a = as[getProbe() & m]) == null ||
!(uncontended = a.cas(v = a.value, v + x)))
longAccumulate(x, null, uncontended);
}
}
//LongAccumulator的accumulate
public void accumulate(long x) {
Cell[] as; long b, v, r; int m; Cell a;
if ((as = cells) ! = null ||
(r = function.applyAsLong(b = base, x)) ! = b && ! casBase(b, r)) {
boolean uncontended = true;
if (as == null || (m = as.length -1) < 0 ||
(a = as[getProbe() & m]) == null ||
!(uncontended =
(r = function.applyAsLong(v = a.value, x)) == v ||
a.cas(v, r)))
longAccumulate(x, function, uncontended);
}
}
另外,前者在调用 longAccumulate 时传递的是function,而后者是 null。
从下面的代码可知,当 fn 为 null 时就使用 v+x 加法运算,这时候就等价于 LongAdder,
当 fn 不为 null 时则使用传递的 fn 函数计算。

总结:本节简单介绍了 LongAccumulator 的原理。LongAdder 类是 LongAccumulator 的一个特例,只是后者提供了更加强大的功能,可以让用户自定义累加规则。
4.4 总结
本章介绍了并发包中的原子性操作类,这些类都是使用非阻塞算法 CAS 实现的,
CAS 即 Compare and Swap,其是 JDK 提供的非阻塞原子性操作,它通过硬件保证了比较---更新操作的原子性。
这相比使用锁实现原子性操作在性能上有很大提高。
首先讲解了最简单的 AtomicLong 类的实现原理,然后讲解了 JDK 8 中新增的 LongAdder 类和 LongAccumulator 类的原理。
学习完本章后,希望读者在实际项目环境中能因地制宜地使用原子性操作类来提升系统性能。