一、什么是CAS?
CAS(Compare And Sweap或者Compare And Set,比较替换),CAS和volatile的读写共同支撑起了整合JUC包。工作原理是:一个CAS操作有三个操作数,目标内存地址V,旧的预期值E和将要替换的新值B,在进行替换操作前比较当且仅当目标内存地址V中的值和旧的预期值E值相等的时候才会发生替换,否则什么都不做。

CAS是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
二、CAS的原理
在Java中通过调用Unsafe类中的JNI代码来支持CAS。它就是借助C/c++调用底层汇编指令实现的。下面从分析比较常用的CPU(intel x86)来解释CAS的实现原理。下面是sun.misc.Unsafe类中几个支持CAS的方法。
我们以compareAndSweapInt()
为例,这个本地方法在openjdk中依次调用的c++代码为:unsafe.cpp,atomic.cpp和atomic_windows_x86.inline.hpp(windows实现)或atomic_linux_x86.inline.hpp(Linux实现)。这个本地方法的最终实现在openjdk的如下位置:openjdk-7-fcs-src-b147-27jun2011\openjdk\hotspot\src\oscpu\windowsx86\vm\ atomicwindowsx86.inline.hpp(对应于windows操作系统,X86处理器)。下面是对应于intel x86处理器的源代码的片段:
// Adding a lock prefix to an instruction on MP machine
// VC++ doesn't like the lock prefix to be on a single line
// so we can't insert a label after the lock prefix.
// By emitting a lock prefix, we can define a label after it.
#define LOCK_IF_MP(mp) __asm cmp mp, 0 \
__asm je L0 \
__asm _emit 0xF0 \
__asm L0:
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
// alternative for InterlockedCompareExchange
int mp = os::is_MP();
__asm {
mov edx, dest
mov ecx, exchange_value
mov eax, compare_value
LOCK_IF_MP(mp)
cmpxchg dword ptr [edx], ecx
}
}
如上面源代码所示,程序会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀。如果程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(lock cmpxchg
)。反之,如果程序是在单处理器上运行,就省略lock前缀(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果)。
intel手册对lock前缀指令的说明如下:
- 确保对内存的读-改-写操作原子执行。在Pentium及Pentium之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其他处理器暂时无法通过总线访问内存。很显然,这会带来昂贵的开销。从Pentium 4,Intel Xeon及P6处理器开始,intel在原有总线锁的基础上做了一个很有意义的优化:如果要访问的内存区域(area of memory)在lock前缀指令执行期间已经在处理器内部的缓存中被锁定(即包含该内存区域的缓存行当前处于独占或以修改状态),并且该内存区域被完全包含在单个缓存行(cache line)中,那么处理器将直接执行该指令。由于在指令执行期间该缓存行会一直被锁定,其它处理器无法读/写该指令要访问的内存区域,因此能保证指令执行的原子性。这个操作过程叫做缓存锁定(cache locking),缓存锁定将大大降低lock前缀指令的执行开销,但是当多处理器之间的竞争程度很高或者指令访问的内存地址未对齐时,仍然会锁住总线。
- 禁止该指令与之前和之后的读和写指令重排序。
- 把写缓冲区中的所有数据刷新到内存中。
关于CPU的锁有如下3种:
- 处理器自动保证基本内存操作的原子性
首先处理器会自动保证基本的内存操作的原子性。处理器保证从系统内存当中读取或者写入一个字节是原子的,意思是当一个处理器读取一个字节时,其他处理器不能访问这个字节的内存地址。奔腾6和最新的处理器能自动保证单处理器对同一个缓存行里进行16/32/64位的操作是原子的,但是复杂的内存操作处理器不能自动保证其原子性,比如跨总线宽度,跨多个缓存行,跨页表的访问。但是处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。
- 使用总线锁保证原子性
第一个机制是通过总线锁保证原子性。如果多个处理器同时对共享变量进行读改写(i++就是经典的读改写操作)操作,那么共享变量就会被多个处理器同时进行操作,这样读改写操作就不是原子的,操作完之后共享变量的值会和期望的不一致。
原因是有可能多个处理器同时从各自的缓存中读取变量i,分别进行加一操作,然后分别写入系统内存当中。那么想要保证读改写共享变量的操作是原子的,就必须保证CPU1读改写共享变量的时候,CPU2不能操作缓存了该共享变量内存地址的缓存。
处理器使用总线锁就是来解决这个问题的。所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占使用共享内存。
- 使用缓存锁保证原子性
第二个机制是通过缓存锁定保证原子性。在同一时刻我们只需保证对某个内存地址的操作是原子性即可,但总线锁定把CPU和内存之间通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,最近的处理器在某些场合下使用缓存锁定代替总线锁定来进行优化。
频繁使用的内存会缓存在处理器的L1,L2和L3高速缓存里,那么原子操作就可以直接在处理器内部缓存中进行,并不需要声明总线锁,在奔腾6和最近的处理器中可以使用“缓存锁定”的方式来实现复杂的原子性。所谓“缓存锁定”就是如果缓存在处理器缓存行中内存区域在LOCK操作期间被锁定,当它执行锁操作回写内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时会起缓存行无效。
但是有两种情况下处理器不会使用缓存锁定。第一种情况是:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行(cache line),则处理器会调用总线锁定。第二种情况是:有些处理器不支持缓存锁定。对于Inter486和奔腾处理器,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定。
以上两个机制我们可以通过Inter处理器提供了很多LOCK前缀的指令来实现。比如位测试和修改指令BTS,BTR,BTC,交换指令XADD,CMPXCHG和其他一些操作数和逻辑指令,比如ADD(加),OR(或)等,被这些指令操作的内存区域就会加锁,导致其他处理器不能同时访问它。
三、CAS的缺陷
CAS可以高效的保证原子性,但是CAS任然存在三大问题:ABA问题、循环时间长开销大和只能保证一个变量的原子性。
1.ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会觉得它的值没有发生变化,但是实际上发没发生变化对于当前线程是不可见的。ABA问题的解决思路就是在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
ABA问题Java提供的解决方案
jdk1.5 Java提供了AtomicStampedReference
compareAndSet
- expectedReference:表示预期值
- newReference:新值
- expectedStamp:预期版本号
- newStamp:更新后的版本号
(1)首先我们可以看看Pair的实现
可以看到,Pair是个静态内部类,他是用来保存预期stamp和预期值引用reference的。在AtomicStampedReference内部有一个Pair对象,并且是被volatile修饰的,保证了在多个线程之间的可见性。
之后就是比较当前pair中的值和预期值是否相等,pare中的时间戳是否和预期时间错相等,如果都相等,最终调用了casPair`方法,源码如下,主要就是通过Unsafe类来更新版本号和新的值。
至于Unsafe类调用的compareAndSwapObject
方法那是一个native方法,在Java源码层面是看不到了,所以就不往下追了。
2. 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
3. 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
四、CAS在Java中的应用——Java原子类
1、原子更新基本类型
J.U.C包下主要提供了以下类可以原子的更新基本类型:
(1)AtomicInteger:原子的更新int类型
(2)AtomicLong:原子的更新long类型
(3)AtomicBoolean:原子更新布尔类型,内部使用int类型的value存储1和0表示true和false,底层也是对int类型的原子操作。
这些类的基本操作和API都很类似,底层都是对Unsafe类的compareAndSwapXxxx()方法的调用,下面是基本的使用:
public class CASBase {
public static void main(String[] args) {
AtomicInteger value = new AtomicInteger(10);
//类似++i操作
System.out.println(value.incrementAndGet());
//类似i++操作
System.out.println(value.getAndIncrement());
//类似--i操作
System.out.println(value.decrementAndGet());
//类似i--操作
System.out.println(value.getAndDecrement());
//先获取当前值,在加50
System.out.println(value.getAndAdd(50));
//先加上50之后在获取值
System.out.println(value.addAndGet(50));
//直接设置值为666,返回是否设置成功
value.compareAndSet(value.get(), 666);
System.out.println(value.get());
//可以进行任何运算(加减乘除模)并且是原子的,他是调用了compareAndSet()实现的CAS操作
System.out.println(value.updateAndGet(x -> x * 100));
}
}
具体的源码分析请参考我的另一篇博文深入源码分析原子类AtomicInteger。
2、原子更新引用类型
J.U.C包下提供了以下类可以用于原子的更新引用类型的引用:
(4)AtomicReference<V>:原子更新引用类型,通过泛型指定要操作的类,存在ABA问题。
(5)AtomicMarkableReference<V>:原子更新引用类型,内部使用Pair承载引用对象及是否被更新过的标记,避免了ABA问题。
(6)AtomicStampedReference<V>:原子更新引用类型,内部使用Pair承载引用对象及更新的时间戳,避免了ABA问题。
public class CASReference {
public static void main(String[] args) {
BigDecimal decimal = new BigDecimal(100);
AtomicStampedReference<BigDecimal> stampedReference = new AtomicStampedReference<>(decimal, 1);
//CAS设置 预期引用是decimal 新的引用是new BigDecimal(200),预期版本是1,新的版本是2
stampedReference.compareAndSet(decimal, new BigDecimal(200), 1, stampedReference.getStamp()+1);
//获得引用,这里是new BigDecimal(200)的引用,他重写了toString()方法因此会打印200
System.out.println(stampedReference.getReference());
//获得当前版本号 应该是2
System.out.println(stampedReference.getStamp());
}
}
具体的源码分析请参考我的另一篇博文深入源码分析原子类AtomicStampedReference。
3、原子更新数组
J.U.C包下提供了以下类:
(7)AtomicIntegerArray:原子更新int类型数组。
(8)AtomicLongArray:原子更新long类型数组。
(9)AtomicReferenceArray:原子更新引用类型数组。
这几个类的操作基本类似,更新元素时都要指定在数组中的索引位置,基本用法如下:
public class CASArray {
public static void main(String[] args) {
//定义一个长度为10的int 类型数组
AtomicIntegerArray array = new AtomicIntegerArray(10);
for (int i = 0; i < array.length(); i++) {
//给索引为i的元素设置值
array.set(i, i * i);
}
for (int i = 0; i < array.length(); i++) {
//获得索引为i的元素
System.out.println(array.get(i));
}
System.out.println("--------------------------------------------------");
//给索引为0的元素做i--操作 操作之后array[0]=-1
System.out.println(array.getAndDecrement(0));
//给索引为0的元素做--i操作 操作之后array[0]=-2
System.out.println(array.decrementAndGet(0));
//给索引为0的元素做i++操作 操作之后array[0]=-1
System.out.println(array.getAndIncrement(0));
//给索引为0的元素做--i操作 操作之后array[0]=0
System.out.println(array.incrementAndGet(0));
//给索引为0的元素加上999 操作之后array[0]=999 ,它返回的是操作之前的值
array.getAndAdd(0, 999);
System.out.println(array.get(0));
//比较并替换索引为0的元素为888 预期值999 新值888,他返回的是替换是否成功
if(array.compareAndSet(0,999,888)){
System.out.println(array.get(0));
}
//可以对索引为0的元素做任何运算操作,第二个参数需要一个lambda表达式
array.updateAndGet(0, (x) -> x * 100);
System.out.println(array.get(0));
}
}
4、原子更新对象中的字段
原子更新对象中的字段,可以更新对象中指定字段名称的字段,这些类主要有:
(10)AtomicIntegerFieldUpdater:原子更新对象中的int类型字段。
(11)AtomicLongFieldUpdater:原子更新对象中的long类型字段。
(12)AtomicReferenceFieldUpdater:原子更新对象中的引用类型字段。
这几个类的操作基本类似,都需要传入要更新的字段名称,基本用法如下:
public class CASObjectField {
public static void main(String[] args) {
//原子更新int类型字段
AtomicIntegerFieldUpdater<User> age = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
//原子更新int类型字段
AtomicLongFieldUpdater<User> stamp = AtomicLongFieldUpdater.newUpdater(User.class, "stamp");
//原子更新int类型字段
AtomicReferenceFieldUpdater<User, String> name = AtomicReferenceFieldUpdater.newUpdater(User.class, String.class, "name");
User user = new User("李四", 23, System.currentTimeMillis());
System.out.println(user);
age.compareAndSet(user,user.getAge(),34);
stamp.compareAndSet(user,user.getStamp(),System.currentTimeMillis());
name.compareAndSet(user,user.getName(),"老王");
System.out.println(user);
}
}
//User类
class User {
volatile String name;
volatile int age;
volatile long stamp;
public User(String name, int age, long stamp) {
this.name = name;
this.age = age;
this.stamp = stamp;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public long getStamp() {
return stamp;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", stamp=" + stamp +
'}';
}
}
5、高性能原子类
高性能原子类是java8中增加的原子类,它们使用分段的思想(Cell[]
),把不同的线程hash到不同的段上去更新,最后再把这些段的值相加得到最终的值,相对Atomic类这些类运行性能更高,这些类主要有:
(1)Striped64:下面四个类的父类。
(2)LongAccumulator:long类型的聚合器,需要传入一个long类型的二元操作,可以用来计算各种聚合操作,包括加减乘除模。
(3)LongAdder:long类型的累加器,LongAccumulator的特例,只能用来计算加法,且从0开始计算。
(4)DoubleAccumulator:double类型的聚合器,需要传入一个double类型的二元操作,可以用来计算各种聚合操作,包括加减乘除模。
(5)DoubleAdder:double类型的累加器,DoubleAccumulator的特例,只能用来计算加法,且从0开始计算。
这几个类的操作基本类似,其中DoubleAccumulator和DoubleAdder底层其实也是用long来实现的,基本用法如下:
public class CASInJDK8 {
public static void main(String[] args) {
LongAdder longAdder = new LongAdder();
longAdder.add(200); //设置值为200
longAdder.increment(); //i++
System.out.println(longAdder.sum()); //获得longAdder的值
LongAccumulator longAccumulator = new LongAccumulator((left, right) -> left+right*2, 0);
longAccumulator.accumulate(34);
System.out.println(longAccumulator.get()); //获得longAccumulator的值
}
}
具体的源码分析请参考我的另一篇博文深入源码分析高性能原子类LongAdder。