16.0 第16章 并发包的基石 16.1 原子变量和CAS

第16章 并发包的基石

15章介绍了线程的基本内容,在Java中还有一套并发工具包,位于包java.util.concurrent下,里面包括很多易用且高性能的并发开发工具。从本章开始,我们就来探讨Java并发工具包。

本章主要介绍并发包的一些基础内容,分为3个小节:16.1节介绍最基本的原子变量及其背后的原理和思维;16.2节介绍可以替代synchronized的显式锁;16.3节介绍可以替代wait/notify的显式条件。

16.1 原子变量和CAS

什么是原子变量?为什么需要它们呢?我们从synchronized说起。在15.2节,我们介绍过Counter类,使用synchronized关键字保证原子更新操作,代码如下:

1
2
3
4
5
6
7
8
9
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文档,我们就不介绍了。

16.1.1 AtomicInteger

我们先介绍AtomicInteger的基本用法,然后介绍它的基本原理和逻辑,以及应用。

1.基本用法

AtomicInteger有两个构造方法:

1
2
public AtomicInteger(int initialValue)
public AtomicInteger()

第一个构造方法给定了一个初始值,第二个构造方法的初始值为0。

可以直接获取或设置AtomicInteger中的值,方法是:

1
2
public final int get()
public final void set(int newValue)

之所以称为原子变量,是因为它包含一些以原子方式实现组合操作的方法,部分方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//以原子方式获取旧值并设置新值
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方法:

1
public final boolean compareAndSet(int expect, int update)

compareAndSet是一个非常重要的方法,比较并设置,我们以后将简称为CAS。该方法有两个参数expect和update,以原子方式实现了如下功能:如果当前值等于expect,则更新为update,否则不更新,如果更新成功,返回true,否则返回false。

AtomicInteger可以在程序中用作一个计数器,多个线程并发更新,也总能实现正确性。我们看个例子,如代码清单16-1所示。

代码清单16-1 AtomicInteger的应用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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。

2.基本原理和思维

AtomicInteger的使用方法是简单直接的,它是怎么实现的呢?它的主要内部成员是:

1
private volatile int value;

注意:它的声明带有volatile,这是必需的,以保证内存可见性

它的大部分更新方法实现都类似,我们看一个方法incrementAndGet,其代码为:

1
2
3
4
5
6
7
8
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:非阻塞并发队列。
  • ConcurrentSkipListMap和ConcurrentSkipListSet:非阻塞并发Map和Set。

这些容器我们在后续章节介绍。

但compareAndSet是怎么实现的呢?我们看代码:

1
2
3
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

它调用了unsafe的compareAndSwapInt方法,unsafe是什么呢?它的类型为sun.misc. Unsafe,定义为:

1
private static final Unsafe unsafe = Unsafe.getUnsafe();

它是Sun的私有实现,从名字看,表示的也是“不安全”,一般应用程序不应该直接使用。原理上,一般的计算机系统都在硬件层次上直接支持CAS指令,而Java的实现都会利用这些特殊指令。从程序的角度看,可以将compareAndSet视为计算机的基本操作,直接接纳就好。

3.实现锁

基于CAS,除了可以实现乐观非阻塞算法之外,还可以实现悲观阻塞式算法,比如锁。实际上,Java并发包中的所有阻塞式工具、容器、算法也都是基于CAS的(不过,也需要一些别的支持)。怎么实现锁呢?我们演示一个简单的例子,用AtomicInteger实现一个锁MyLock,如代码清单16-2所示。

代码清单16-2 使用AtomicInteger实现锁MyLock
1
2
3
4
5
6
7
8
9
10
11
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。

16.1.2 ABA问题

使用CAS方式更新有一个ABA问题。该问题是指,假设当前值为A,如果另一个线程先将A修改成B,再修改回成A,当前线程的CAS操作无法分辨当前值发生过变化。

ABA是不是一个问题与程序的逻辑有关,一般不是问题。而如果确实有问题,解决方法是使用AtomicStampedReference,在修改值的同时附加一个时间戳,只有值和时间戳都相同才进行修改,其CAS方法声明为:

1
2
public boolean compareAndSet(
V expectedReference, V newReference, int expectedStamp, int newStamp)

比如:

1
2
3
4
5
6
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会将两个值组合为一个对象,修改的是一个值,我们看代码:

1
2
3
4
5
6
7
8
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的一个内部类,成员包括引用和时间戳,具体定义为:

1
2
3
4
5
6
7
8
9
10
11
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单个值的比较和修改。

16.1.3 小结

本节介绍了原子变量的基本用法以及背后的原理CAS,对于并发环境中的计数、产生序列号等需求,应该使用原子变量而非锁,CAS是Java并发包的基础,基于它可以实现高效的、乐观、非阻塞式数据结构和算法,它也是并发包中锁、同步工具和各种容器的基础