源码解读(二):Java中的CAS应用
1、什么是CAS
CAS全称为ConmpareAndSwap:比较和交换;
CAS有三个要素原始值、期望值和更新值
操作逻辑:将原始值与期望值进行比较,只有在值相同的情况下,将改原始值更新为新值。
CAS是实现多线程同步的原子指令,具有原子性。
原子性:一个操作不能被打断,要么全部执行完毕,要么不执行。
我们可以把它看成一个被加锁的代码块,在这个代码块中CAS执行了两步操作。
1、比较原值和期望值
2、原值更新为新值
与同步代码块不同时,代码块如果获取不到锁会阻塞,而CAS若比较失败,会返回false,不进行更新。
2、CAS在Java中的使用
为了方便介绍,我们以AtomicInteger类为例。
// value属性在AtomicInteger类的下标位置
private static final long valueOffset;
static {
try {
// 获取value属性在AtomicInteger类的下标位置
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
public final boolean compareAndSet(int expect, int update) {
// java中的CAS方法
// this 当前对象,valueOffset 属性下标,expect 期望值,update 更新值
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
我们进入unsafe类看看,一共有三个CAS方法。
//
/**
* 对Object类型进行操作
* 获取var1对象中var2下标位置的属性值(原值),与期望值var4进行对比,若相同原值更新为更新值var5
* var1 对象
* var2 对象var1中var2下标位置
* var4 期望值
* var5 更新值
* return true 对比相等,更新成功;false 对比不相等,更新失败。
*/
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
// 对int类型进行操作
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
// 对long类型进行操作
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
这个三个CAS方法看似简简单单,却在JUC起着至关重要的做,并发工具类多处对数据的原子操作都能看见CAS的影子。
3、ABA问题
线程1从内存中取出值为A,线程2也从内存中取出值为A,线程2进行CAS操作值为B,线程2有进行CAS操作值为A,这时线程1进行CAS操作时能够成功。但是我们不能认为这是正确的。
就像我们有一百块,拿去吃饭了,后来又挣了一百块,虽然最后还是一百块,可是咱们已经吃饱饭了呀。
为了应对ABA的问题,JAVA中提供了AtomicStampedReference/AtomicMarkableReference,在对象中再加一个标识来记录对象是否发生了改变。
就如同,我们记个账,虽然手里还是一百块,可是账本告诉我们这中间发生了一些故事。
3、ABA问题如何解决
AtomicStampedReference的使用
public static void main(String[] args) {
AtomicStampedReference asr = new AtomicStampedReference(100, 0);
asr.compareAndSet(100, 101, asr.getStamp(), asr.getStamp() + 1);
}
在上面的代码我们可以看到,AtomicStampedReference的CAS一共有四个参数,不仅要比较引用的值,还是比较预期的标记(stamp)是否变更过。下面我们看一下AtomicStampedReference的源码。
public class AtomicStampedReference<V> {
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);
}
}
// 把reference和stamp封装成Pair对象
private volatile Pair<V> pair;
/**
* 对引用和标记进行比较,只有原值同时和期望值相等时,CAS操作才会成功。这里我们不会让stamp出现ABA的情形。
* 也就避免了引用值ABA调用出现的问题。
* expectedReference 期望引用值
* newReference 新引用值
* expectedStamp 期望标记值
* newStamp 新标记值
* return CAS结果,true操作成功,false操作失败
*/
public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) {
Pair<V> current = pair;
return
//1.原引用值与期望引用值相等进入2,否则返回false
expectedReference == current.reference &&
//2.原标记值与期望标记值相等进入3,否则返回false
expectedStamp == current.stamp &&
//3.新引用值和新标记值 分别 原引用值和原标记值相等返回true,否则进入4
((newReference == current.reference && newStamp == current.stamp) ||
//4.CAS操作Pair对象成功返回true,否则返回false
casPair(current, Pair.of(newReference, newStamp)));
}
// 获取UNSAFE对象
private static final sun.misc.Unsafe UNSAFE = sun.misc.Unsafe.getUnsafe();
// 获取属性pair在AtomicStampedReference类中下标位置
private static final long pairOffset = objectFieldOffset(UNSAFE, "pair", AtomicStampedReference.class);
// 调用UNSAFE的CAS方法进行CAS操作
private boolean casPair(Pair<V> cmp, Pair<V> val) {
return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}
// 获取属性pair在AtomicStampedReference类中下标位置
static long objectFieldOffset(sun.misc.Unsafe UNSAFE, String field, Class<?> klazz) {
try {
return UNSAFE.objectFieldOffset(klazz.getDeclaredField(field));
} catch (NoSuchFieldException e) {
// Convert Exception to corresponding Error
NoSuchFieldError error = new NoSuchFieldError(field);
error.initCause(e);
throw error;
}
}
}
走读代码我们可以了解到,其实只是对CAS多引入了一个变量,让这个变量专门来记录标记我们对AtomicStampedReference的操作,避免出现了值被改回去了我们无法发现的问题。
AtomicMarkableReference的方式与AtomicStampedReference类似,相比AtomicStampedReference而言,AtomicMarkableReference把标记值由int型换成了boolean,感兴趣的同学可以自行研究一下源码,这里不再赘述。