专栏原创出处:github-源笔记文件 ,github-源码 ,欢迎 Star,转载请附上原文出处链接和本声明。
Java 并发编程专栏系列笔记,系统性学习可访问个人复盘笔记-技术博客 Java 并发编程
CAS 是英文单词 Compare And Swap 的缩写,翻译过来就是比较并替换。Java 的 compareAndSet(jdk13)/compareAndSwap(jdk1.8) 相关方法调用简称为 CAS。
CAS 机制当中使用了 3 个基本操作数:内存地址 V,旧的预期值 A,要修改的新值 B。更新一个变量的时候,只有当变量的预期值 A 和内存地址 V 当中的实际值相同时,才会将内存地址 V 对应的值修改为 B。
CAS 操作的是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
JDK 文档对 compareAndSet() 方法说明:如果当前状态值等于预期值,则以原子方式将同步状态 设置为给定的更新值。此操作具有 volatile 读和写的内存语义。
以 Unsafe#compareAndSetInt 方法为例进行说明
@HotSpotIntrinsicCandidate
public final native boolean compareAndSwapInt(Object o, long offset,
int expected,
int x);
方法解读:
volatile int value
的字段)实际使用示例(AtomicInteger#compareAndSet):
public class AtomicInteger extends Number implements java.io.Serializable {
// Unsafe 类定义
private static final Unsafe unsafe = Unsafe.getUnsafe();
// 获取变量 value 在内存中对应的地址偏移量
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;
// expectedValue 预期值、newValue 修改值
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
分析 Java 中Unsafe#compareAndSwapInt
方法(jdk1.8) 。
native 方法最终调用实现hotspot/src/os_cpu/linux_x86/vm/atomic_linux_x86.inline.hpp
中Atomic::cmpxchg
实现
inline jint Atomic::cmpxchg(jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::is_MP();
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
}
程序会根据当前处理器的类型来决定是否为 cmpxchg 指令添加 lock 前缀。
intel 的手册对 lock 前缀的说明如下:
上面的第 2 点和第 3 点所具有的内存屏障效果,足以同时实现 volatile 读和 volatile 写的内存语义。
经过上面的分析,现在我们终于能明白为什么 JDK 文档说 CAS 同时具有 volatile 读和 volatile 写的内存语义了。
因为 CAS 需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是 A,变成了 B,又变成了 A,那么使用 CAS 进行检查时会发现它的值没有发生变化,但是实际上却变化了。
ABA 问题的解决思路就是使用版本号。在变量前面 追加上版本号,每次变量更新的时候把版本号加 1,那么 A→B→A 就会变成 1A→2B→3A。从 Java 1.5 开始,JDK 的 Atomic 包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。
这个类的 compareAndSet 方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
自旋 CAS 如果长时间不成功,会给 CPU 带来非常大的执行开销。
如果 JVM 能支持处理器提供的 pause 指令,那么效率会有一定的提升。
pause 指令有两个作用:
当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。
还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量 i=2,j=a,合并一下 ij=2a,然后用 CAS 来操作 ij。从 Java 1.5 开始,JDK 提供了 AtomicReference 类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行 CAS 操作。
CAS 实现原子操作的三大问题
,保证存在问题与需求不会冲突volatile
是否满足需求 ?