一、解释
CAS的全称是compare-and-swap,即比较和交换。虽然看起来的先比较再交换,无法保证原子性,其实其利用的是底层硬件,是一条CPU的原子指令,是线程安全的。jdk中的juc包中的线程安全性,都是基于CAS实现的。其处理过程是:CAS操作需要输出两个数值,其一是旧值(操作前的值),其二是新值,在操作期间先比较一下旧值是否发生的变化,如果没有变化,则用新值替换旧值,否则不交换。
二、源码解析
基于AtomicInteger类,来看看CAS的具体过程。
public class AtomicInteger extends Number implements java.io.Serializable {
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); }
}
// volatile保存了value的可见性
private volatile int value;
public AtomicInteger(int initialValue) {
value = initialValue;
}
public AtomicInteger() {
}
// 获取当前值
public final int get() {
return value;
}
// 设置新值
public final void set(int newValue) {
value = newValue;
}
// 设置新值并返回旧值
public final int getAndSet(int newValue) {
return unsafe.getAndSetInt(this, valueOffset, newValue);
}
// 比较交换
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
...
}
可以看到AtomicInteger底层利用volatile和cas来进行更新数据。volatile修饰的value,保证了value更改之后,其他线程立马就能知道。Unsafe类提供的cas操作,保证了整个更新过程的原子性。
下面再看看Unsafe类的源码
public final class Unsafe {
...
/**
*
* @param o 待更新的对象
* @param offset 偏移量,不用管这个,c++底层会用到
* @param newValue 新增
* @return 先前的值
* @since 1.8
*/
public final int getAndSetInt(Object o, long offset, int newValue) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, newValue));
return v;
}
...
public final native boolean compareAndSwapInt(Object o, long offset,
int expected,
int x);
...
}
其中的方法compareAndSwapInt()是被natice修饰的本地方法,我们看不到方法的具体操作,其实它是由C++实现的本地方法,是一个原子性的比较交换操作。compareAndSwapInt()操作成功返回true,否则返回false。那么AtomicInteger是如何实现更新操作的呢,可以看getAndSetInt()方法。代码很简单,先取内存中的当前值v = getIntVolatile(o, offset),然后调用本地方法compareAndSwapInt去更新为新值newValue,如果失败,则重新获最当前值再去更新,这样一直循环下去,直到更新成功。
以上,就是AtomicInteger如何利用CAS来保证线程安全的。
三、缺点总结
1、ABA问题。CAS比较交换时,是检查当前值与期望值是否一致。试想一下,如果某个值由A变成了B,再由B变回了A,那么在做CAS比较时,会认为值没有变化,但实际是发生了变化。ABA问题的解决思路是给数据加一个版本号,每次更新后对其版本加1,这样在值变回A之后,其版本已不是原来的版本了。具体可参见jdk中的AtomicStampedReference
2、开销大。在高并发情况下,自旋CAS如果长时间不成功,会一直执行循环操作,给CPU带来非常大的执行开销。所以其适用于那些并发不是很大的场景。