java并发之CAS详解
前言
在高并发的应用当中,最关键的问题就是对共享变量的安全访问,通常我们都是通过加锁的方式,比如说synchronized、Lock来保证原子性,或者在某些应用当中,用voliate来保证变量的可见性,还有就是通过TheadLocal将变量copy一份,称为局部变量(线程私有)等等。现在我们学习一种不加锁机制(CAS)
上述我们提到的synchronized和Lock这都是通过加锁实现的(悲观锁),其实加锁本质上是将并发转变成串行实现的,势必会阻塞线程的执行,影响应用的吞吐量,而CAS正是一种乐观的策略,并不会出现加锁来阻塞线程的执行。
CAS简介
CAS,Compare And Swap,即比较并交换。Atomic原子类操作等等都是以CAS实现的,还有ConcurrentHashMap在JDK1.8的版本也调整为了CAS+Synchronized
分析(自旋中对应下文的native方法)
在CAS中有三个参数:内存值V、旧的预期值A、要更新的值B。当且仅当内存值V的值等于旧的预期值A时才会将内存值V的值修改为B,否则什么都不干
伪代码如下:
if(this.value == A){
this.value = B
return true;
}else{
return false;
}
应用
在java.util.concurrent.atomic包下原子类都是通过CAS来实现的,现在我们以AtomicInteger为例来分析一下CAS的实现
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
Unsafe是CAS的核心类,Java无法直接访问底层操作系统,而是通过本地(native)方法来访问。不过尽管如此,JVM还是开了一个后门:Unsafe,它提供了硬件级别的原子操作。
valueOffset为变量值在内存中的偏移地址,unsafe就是通过偏移地址来得到数据的原值的。
value当前值,使用volatile修饰,保证多线程环境下看见的是同一个。
下面我们以AtomicInteger的addAndGet()方法来说明
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
内部调用的是Unsafe类的getAndAddInt()方法
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
而在getAndAddInt方法中又调用了Unsafe的本地方法
public native int getIntVolatile(Object var1, long var2);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
此方法是本地方法(java需要调用其他语言的代码,比如C语言代码,跟dll文件有关),有四个参数,分别代表:对象、对象的地址、预期值、修改值
CAS的缺陷
-
乐观锁只能保证一个共享变量的原子操作。如上例子,自旋过程中只能保证value变量的原子性,这时如果多一个或几个变量,乐观锁将变得力不从心,但互斥锁能轻易解决,不管对象数量多少及对象颗粒度大小。
-
长时间自旋可能导致开销大。假如CAS长时间不成功而一直自旋,会给CPU带来很大的开销。
-
ABA问题。CAS的核心思想是通过比对内存值与预期值是否一样而判断内存值是否被改过,但这个判断逻辑不严谨,假如内存值原来是A,后来被一条线程改为B,最后又被改成了A,则CAS认为此内存值并没有发生改变,但实际上是有被其他线程改过的,这种情况对依赖过程值的情景的运算结果影响很大。解决的思路是引入版本号,每次变量更新都把版本号加一。
synchronized与CAS的区别
-
对于资源竞争较少的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
-
对于资源竞争严重的情况,CAS自旋的概率会比较大(比如getAndAddInt方法中的do-while循环),从而浪费更多的CPU资源,效率低于synchronized。