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 | public class Counter { |
对于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 | public AtomicInteger(int initialValue) |
第一个构造方法给定了一个初始值,第二个构造方法的初始值为0。
可以直接获取或设置AtomicInteger中的值,方法是:
1 | public final int get() |
之所以称为原子变量,是因为它包含一些以原子方式实现组合操作的方法,部分方法如下:
1 | //以原子方式获取旧值并设置新值 |
这些方法的实现都依赖另一个public方法:
1 | public final boolean compareAndSet(int expect, int update) |
compareAndSet是一个非常重要的方法,比较并设置,我们以后将简称为CAS。该方法有两个参数expect和update,以原子方式实现了如下功能:如果当前值等于expect,则更新为update,否则不更新,如果更新成功,返回true,否则返回false。
AtomicInteger可以在程序中用作一个计数器,多个线程并发更新,也总能实现正确性。我们看个例子,如代码清单16-1所示。
1 | public class AtomicIntegerDemo { |
程序的输出总是正确的,为1000000。
2.基本原理和思维
AtomicInteger的使用方法是简单直接的,它是怎么实现的呢?它的主要内部成员是:
1 | private volatile int value; |
注意:它的声明带有volatile,这是必需的,以保证内存可见性。
它的大部分更新方法实现都类似,我们看一个方法incrementAndGet,其代码为:
1 | public final int incrementAndGet() { |
代码主体是个死循环,先获取当前值current,计算期望的值next,然后调用CAS方法进行更新,如果更新没有成功,说明value被别的线程改了,则再去取最新值并尝试更新直到成功为止。
与synchronized锁相比,这种原子更新方式代表一种不同的思维方式。synchronized是悲观的,它假定更新很可能冲突,所以先获取锁,得到锁后才更新。原子变量的更新逻辑是乐观的,它假定冲突比较少,但使用CAS更新,也就是进行冲突检测,如果确实冲突了,那也没关系,继续尝试就好了。synchronized代表一种阻塞式算法,得不到锁的时候,进入锁等待队列,等待其他线程唤醒,有上下文切换开销。原子变量的更新逻辑是非阻塞式的,更新冲突的时候,它就重试,不会阻塞,不会有上下文切换开销。对于大部分比较简单的操作,无论是在低并发还是高并发情况下,这种乐观非阻塞方式的性能都远高于悲观阻塞式方式。
原子变量相对比较简单,但对于复杂一些的数据结构和算法,非阻塞方式往往难于实现和理解,幸运的是,Java并发包中已经提供了一些非阻塞容器,我们只需要会使用就可以了,比如:
- ConcurrentLinkedQueue和ConcurrentLinkedDeque:非阻塞并发队列。
- ConcurrentSkipListMap和ConcurrentSkipListSet:非阻塞并发Map和Set。
这些容器我们在后续章节介绍。
但compareAndSet是怎么实现的呢?我们看代码:
1 | public final boolean compareAndSet(int expect, int 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所示。
1 | public class MyLock { |
在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 | public boolean compareAndSet( |
比如:
1 | Pair pair = new Pair(100, 200); |
AtomicStampedReference在compareAndSet中要同时修改两个值:一个是引用,另一个是时间戳。它怎么实现原子性呢?实际上,内部AtomicStampedReference会将两个值组合为一个对象,修改的是一个值,我们看代码:
1 | public boolean compareAndSet(V expectedReference, V newReference,int expectedStamp, int newStamp) { |
这个Pair是AtomicStampedReference的一个内部类,成员包括引用和时间戳,具体定义为:
1 | private static class Pair<T> { |
AtomicStampedReference将对引用值和时间戳的组合比较和修改转换为了对这个内部类Pair单个值的比较和修改。
16.1.3 小结
本节介绍了原子变量的基本用法以及背后的原理CAS,对于并发环境中的计数、产生序列号等需求,应该使用原子变量而非锁,CAS是Java并发包的基础,基于它可以实现高效的、乐观、非阻塞式数据结构和算法,它也是并发包中锁、同步工具和各种容器的基础。