原子变量和CAS
什么是原子变量?为什么需要它们呢?我们从synchronized说起。介绍过Counter类,使用synchronized关键字保证原子更新操作,代码如下:
java
public class Counter {
private int count;
public synchronized void incr(){
count++;
}
public synchronized int getCount(){
return count;
}
}
对于count++这种操作来说,使用synchronized成本太高了,需要先获取锁,最后需要释放锁,获取不到锁的情况下需要等待,还会有线程的上下文切换,这些都需要成本。
对于这种情况,完全可以使用原子变量代替,Java并发包中的基本原子变量类型有以下几种。
- AtomicBoolean:原子Boolean类型,常用来在程序中表示一个标志位。
- AtomicInteger:原子Integer类型。
- AtomicLong:原子Long类型,常用来在程序中生成唯一序列号。
- AtomicReference:原子引用类型,用来以原子方式更新复杂类型。
限于篇幅,主要介绍AtomicInteger。除了这4个
类,还有一些其他类,如针对数组类型的类AtomicLongArray、AtomicReferenceArray,以及用于以原子方式更新对象中的字段的类,如AtomicIntegerFieldUpdater、AtomicReferenceFieldUpdater等。Java 8增加了几个类,在高并发统计汇总的场景中更为适合,包括LongAdder、LongAccumulator、Double-Adder和DoubleAccumulator,具体可参见API文档。
1、AtomicInteger
1.1、基本用法
AtomicInteger有两个构造方法:
java
public AtomicInteger(int initialValue)
public AtomicInteger()
第一个构造方法给定了一个初始值,第二个构造方法的初始值为0。
可以直接获取或设置AtomicInteger中的值,方法是:
java
public final int get()
public final void set(int newValue)
之所以称为原子变量,是因为它包含一些以原子方式实现组合操作的方法,部分方法如下:
java
//以原子方式获取旧值并设置新值
public final int getAndSet(int newValue)
//以原子方式获取旧值并给当前值加1
public final int getAndIncrement()
//以原子方式获取旧值并给当前值减1
public final int getAndDecrement()
//以原子方式获取旧值并给当前值加delta
public final int getAndAdd(int delta)
//以原子方式给当前值加1并获取新值
public final int incrementAndGet()
//以原子方式给当前值减1并获取新值
public final int decrementAndGet()
//以原子方式给当前值加delta并获取新值
public final int addAndGet(int delta)
这些方法的实现都依赖另一个public方法:
java
public final boolean compareAndSet(int expect, int update)
compareAndSet是一个非常重要的方法,比较并设置,我们以后将简称为CAS。该方法有两个参数expect和update,以原子方式实现了如下功能:如果当前值等于expect,则更新为update,否则不更新,如果更新成功,返回true,否则返回false。
AtomicInteger可以在程序中用作一个计数器,多个线程并发更新,也总能实现正确性。我们看个例子,如代码所示。
java
public class AtomicIntegerDemo {
private static AtomicInteger counter = new AtomicInteger(0);
static class Visitor extends Thread {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
counter.incrementAndGet();
}
}
}
public static void main(String[] args) throws InterruptedException {
int num = 1000;
Thread[] threads = new Thread[num];
for (int i = 0; i < num; i++) {
threads[i] = new Visitor();
threads[i].start();
}
for (int i = 0; i < num; i++) {
threads[i].join();
}
System.out.println(counter.get());
}
}
程序的输出总是正确的,为1000000。
1.2、基本原理和思维
AtomicInteger的使用方法是简单直接的,它是怎么实现的呢?它的主要内部成员是:
java
private volatile int value;
注意:它的声明带有volatile,这是必需的,以保证内存可见性。
它的大部分更新方法实现都类似,我们看一个方法incrementAndGet,其代码为:
java
public final int incrementAndGet() {
for(; ; ) {
int current = get();
int next = current + 1;
if(compareAndSet(current, next))
return next;
}
}
代码主体是个死循环,先获取当前值current,计算期望的值next,然后调用CAS方法进行更新,如果更新没有成功,说明value被别的线程改了,则再去取最新值并尝试更新直到成功为止。
与synchronized锁相比,这种原子更新方式代表一种不同的思维方式。synchronized是悲观的,它假定更新很可能冲突,所以先获取锁,得到锁后才更新。原子变量的更新逻辑是乐观的,它假定冲突比较少,但使用CAS更新,也就是进行冲突检测,如果确实冲突了,那也没关系,继续尝试就好了。synchronized代表一种阻塞式算法,得不到锁的时候,进入锁等待队列,等待其他线程唤醒,有上下文切换开销。原子变量的更新逻辑是非阻塞式的,更新冲突的时候,它就重试,不会阻塞,不会有上下文切换开销。对于大部分比较简单的操作,无论是在低并发还是高并发情况下,这种乐观非阻塞方式的性能都远高于悲观阻塞式方式。
原子变量相对比较简单,但对于复杂一些的数据结构和算法,非阻塞方式往往难于实现和理解,幸运的是,Java并发包中已经提供了一些非阻塞容器,我们只需要会使用就可以了,比如:
- ConcurrentLinkedQueue和ConcurrentLinkedDeque:非阻塞并发队列。
- ConcurrentSkipListMapConcurrentSkipListSet:非阻塞并发Map和Set。
但compareAndSet是怎么实现的呢?我们看代码:
java
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
它调用了unsafe的compareAndSwapInt方法,unsafe是什么呢?它的类型为sun.misc. Unsafe,定义为:
java
private static final Unsafe unsafe = Unsafe.getUnsafe();
它是Sun的私有实现,从名字看,表示的也是"不安全",一般应用程序不应该直接使用。原理上,一般的计算机系统都在硬件层次上直接支持CAS指令,而Java的实现都会利用这些特殊指令。从程序的角度看,可以将compareAndSet视为计算机的基本操作,直接接纳就好。
1.3、实现锁
基于CAS,除了可以实现乐观非阻塞算法之外,还可以实现悲观阻塞式算法,比如锁。实际上,Java并发包中的所有阻塞式工具、容器、算法也都是基于CAS的(不过,也需要一些别的支持)。怎么实现锁呢?我们演示一个简单的例子,用AtomicInteger实现一个锁MyLock,如代码所示。
java
public class MyLock {
private AtomicInteger status = new AtomicInteger(0);
public void lock() {
while (!status.compareAndSet(0, 1)) {
Thread.yield();
}
}
public void unlock() {
status.compareAndSet(1, 0);
}
}
在MyLock中,使用status表示锁的状态,0表示未锁定,1表示锁定,lock()、unlock()使用CAS方法更新,lock()只有在更新成功后才退出,实现了阻塞的效果,不过一般而言,这种阻塞方式过于消耗CPU,我们后续章节介绍更为高效的方式。MyLock只是用于演示基本概念,实际开发中应该使用Java并发包中的类,如ReentrantLock。
2、ABA问题
使用CAS方式更新有一个ABA问题。该问题是指,假设当前值为A,如果另一个线程先将A修改成B,再修改回成A,当前线程的CAS操作无法分辨当前值发生过变化。
ABA是不是一个问题与程序的逻辑有关,一般不是问题。而如果确实有问题,解决方法是使用AtomicStampedReference,在修改值的同时附加一个时间戳,只有值和时间戳都相同才进行修改,其CAS方法声明为:
java
public boolean compareAndSet(
V expectedReference, V newReference, int expectedStamp, int newStamp)
比如:
java
Pair pair = new Pair(100, 200);
int stamp = 1;
AtomicStampedReference<Pair> pairRef = new
AtomicStampedReference<Pair>(pair, stamp);
int newStamp = 2;
pairRef.compareAndSet(pair, new Pair(200, 200), stamp, newStamp);
AtomicStampedReference在compareAndSet中要同时修改两个值:一个是引用,另一个是时间戳。它怎么实现原子性呢?实际上,内部AtomicStampedReference会将两个值组合为一个对象,修改的是一个值,我们看代码:
java
public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));}
这个Pair是AtomicStampedReference的一个内部类,成员包括引用和时间戳,具体定义为:
java
private static class Pair<T> {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
AtomicStampedReference将对引用值和时间戳的组合比较和修改转换为了对这个内部类Pair单个值的比较和修改。