第4章 Java并发包中原子操作类原理剖析
JUC包提供了一系列的原子性操作类,这些类都是使用非阻塞算法CAS实现的,相比使用锁实现原子性操作这在性能上有很大提高。由于原子性操作类的原理都大致相同,所以本章只讲解最简单的AtomicLong类的实现原理以及JDK 8中新增的LongAdder和LongAccumulator类的原理。有了这些基础,再去理解其他原子性操作类的实现就不会感到困难了。
4.1 原子变量操作类
JUC并发包中包含有AtomicInteger、AtomicLong和AtomicBoolean等原子性操作类,它们的原理类似,本章讲解AtomicLong类。AtomicLong是原子性递增或者递减类,其内部使用Unsafe来实现,我们看下面的代码。
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.递增和递减操作代码
//(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中的实现逻辑为
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中的逻辑为
public final long getAndIncrement() {
return unsafe.getAndAddLong(this, valueOffset, 1L);
}
其中JDK 8中unsafe.getAndAddLong的代码为
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)方法
public final boolean compareAndSet(long expect, long update) {
return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
}
由如上代码可知,在内部还是调用了unsafe.compareAndSwapLong方法。如果原子变量中的value值等于expect,则使用update值更新该值并返回true,否则返回false。
下面通过一个多线程使用AtomicLong统计0的个数的例子来加深对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());
}
}
输出结果为
count 0:7
如上代码中的两个线程各自统计自己所持数据中0的个数,每当找到一个0就会调用AtomicLong的原子性递增方法。
在没有原子类的情况下,实现计数器需要使用一定的同步措施,比如使用synchronized关键字等,但是这些都是阻塞算法,对性能有一定损耗,而本章介绍的这些原子操作类都使用CAS非阻塞算法,性能更好。但是在高并发情况下AtomicLong还会存在性能问题。JDK 8提供了一个在高并发下性能更好的LongAdder类,下面我们来讲解这个类。
4.2 JDK8新增的原子操作类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的构造。
@sun.misc.Contended static final class Cell {
volatile long value;
Cell(long x) { value = x; }
final boolean cas(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
}
// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
private static final long valueOffset;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<? > ak = Cell.class;
valueOffset = UNSAFE.objectFieldOffset
(ak.getDeclaredField("value"));
} catch (Exception e) {
throw new Error(e);
}
}
}
可以看到,Cell的构造很简单,其内部维护一个被声明为volatile的变量,这里声明为volatile是因为线程操作value变量时没有使用锁,为了保证变量的内存可见性这里将其声明为volatile的。另外cas函数通过CAS操作,保证了当前线程更新时被分配的Cell元素中value值的原子性。另外,Cell类使用@sun.misc.Contended修饰是为了避免伪共享。到这里我们回答了问题1和问题6。
● long sum()返回当前的值,内部操作是累加所有Cell内部的value值后再累加base。例如下面的代码,由于计算总和时没有对Cell数组进行加锁,所以在累加过程中可能有其他线程对Cell中的值进行了修改,也有可能对数组进行了扩容,所以sum返回的值并不是非常精确的,其返回值并不是一个调用sum方法时的原子快照值。
public long sum() {
Cell[] as = cells; Cell a;
long sum = base;
if (as ! = null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) ! = null)
sum += a.value;
}
}
return sum;
}
● void reset()为重置操作,如下代码把base置为0,如果Cell数组有元素,则元素值被重置为0。
public void reset() {
Cell[] as = cells; Cell a;
base = 0L;
if (as ! = null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) ! = null)
a.value = 0L;
}
}
}
● long sumThenReset()是sum的改造版本,如下代码在使用sum累加对应的Cell值后,把当前Cell的值重置为0, base重置为0。这样,当多线程调用该方法时会有问题,比如考虑第一个调用线程清空Cell的值,则后一个线程调用时累加的都是0值。
public long sumThenReset() {
Cell[] as = cells; Cell a;
long sum = base;
base = 0L;
if (as ! = null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) ! = null) {
sum += a.value;
a.value = 0L;
}
}
}
return sum;
}
● long longValue()等价于sum()。
下面主要看下add方法的实现,从这个方法里面就可以找到其他问题的答案。
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数组被初始化和扩容的地方。
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计算当前线程应该访问celll数组的哪个位置,也就是使用当前线程的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 累加器的初始值。
publ工C LongAccumul ator (LongBinaryOper ator accumulatorFunct工on , long ident工ty ) {
this . function = accumulatorFunction;
base = this . identity= identity;
}
publ工C interface LongBinaryOperator {
//根据两个参数计算并返回一个值
long applyAsLo口g ( long left, long right ) ;
}
上面提到, LongAdder 其实是 LongAccumulator 的一个特例, 调用 LongAdder 就相当 于使用下面的方式调用
LongAdder adder= new LongAdder () ;
LongAccumul ator accumulator= new LongAccumulator(new Lo口gBinaryOperator( ) {
@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) 来计算。
//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 时传递的是 且mction, 而后者是 null。 从下面的代码 可知, 当 fn 为 null 时-就使用 v+x 加法运算,这时候就等价于 LongAdder, 当 fn 不为 null 时则使用传递的 fn 函数计算。
else if (casBase (v = base, ( (fn == null) ? v + x : fn .applyAsLong (v , x ))))
break; //Fall back on using base
总结 : 本节简单介绍 了 LongAccumulator 的原理。 LongAdder 类是 LongAccumulator 的一个特例,只是后者提供了更加强大的功能, 可以让用户自定义累加规则。
4.4 总结
本章介绍了并发包中的原子性操作类,这些类都是使用非阻塞算法 CAS 实现的, 这 相 比使用锁实现原子性操作在性能上有很大提高。首先讲解了最简单的 AtomicLong 类的 实现原理, 然后讲解了 JDK 8 中新增的 LongAdder 类和 LongAccumulator 类的原理。学习 完本章后, 希望读者在实际项 目环境中能因地制直地使用原子性操作类来提升系统性能。