一、CAS概念
CAS(compare and swap):比较并交换,CAS操作包含三个操作数,内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。同时CPU的一个原语操作,在intel的CPU中,使用cmpxchg指令。在JAVA中就是通过JNI对该原语的调用实现CAS。sun.misc.Unsafe 类中
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
原语:内核或微核提供核外调用的过程或函数称为原语(primitive);原语是一段用机器指令编写的完成特定功能的程序,在执行过程中不允许中断。
CPU中CAS的实现原理:如果程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(lock cmpxchg);如果程序是在单处理器上运行,就省略lock前缀(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果)。
intel的手册对lock前缀的说明如下:
- 确保对内存的读-改-写操作原子执行。
- 禁止该指令与之前和之后的读和写指令重排序。
- 把写缓冲区中的所有数据刷新到内存中,保证可见性。
二、CAS的缺点
循环占用CPU资源问题:由于CAS本身不会使线程进入阻塞,他是通过循环不断的比较,在高并发场景下,如果循环多次依旧无法更新,会给CPU带来很大的计算开销
ABA问题:在并发环境中,线程A先从内存中取出数据,一段时间后;再进行CAS时虽然比较成功,但是与当前值对比的内存中的值已经不是当初的哪个值了。
例如:小牛取款,由于机器不太好使,多点了几次取款操作。后台threadA和threadB同时读取到的值都是100,此时threadA阻塞,threadB操作成功(100->50)。正好牛妈打款50元给小牛(50->100),threadC执行成功,之后threadA运行了,又改为(100->50)。如此玩法就会发现小牛少了50大洋(正确:100-50+50=100)
如何解决aba问题:
对内存中的值加个版本号,在比较的时候除了比较值还的比较版本号;例如java:AtomicStampedReferenc
三、AtomicStampedReferenc 源码
数据结构:
private static class Pair<T> {
final T reference;//存放设置的数据
final int stamp;//存放设置数据的版本号
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
//构造 Pair对象
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
构造函数:
//AtomicStampedReference 构造函数
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
}
比较置换:compareAndSet
// expectedReference:预期对象 newReference:新的对象
// expectedStamp:预期对象的版本号 newStamp:预期对象的版本号
public boolean compareAndSet(V expectedReference, V newReference,int expectedStamp,int newStamp) {
Pair<V> current = pair;//当前Pari对象
return
expectedReference == current.reference //预期对象与当前对象是否相等
&& expectedStamp == current.stamp //预期对象的版本号与当前对象的版本号是否相等
//新对象与当前对象相等,并且他们的版本号也相等 或者 执行CSA成功
&&((newReference == current.reference && newStamp == current.stamp) || casPair(current, Pair.of(newReference, newStamp)));
}