定义
CAS
CAS(Compare-And-Swap)是一种无锁的原子算法,是乐观锁的核心实现机制。
它是CPU提供的一条原子指令,包含3个基本操作数:
- V:需要读写的内存位置。
- A:预期的旧值。
- B:拟写入的新值。
Java实现
Unsafe
Unsafe中存在一系列native方法,能够直接操作内存和调用CPU指令。
public final class Unsafe {
public final native boolean compareAndSwapInt(Object o, long offset,
int expected,
int x);
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset); // 拿到volatile变量最新值
} while (!compareAndSwapInt(o, offset, v, v + delta)); // CAS失败时不断循环重试
return v;
}
}AtomicInteger(JDK 8)
相关原子类会调用Unsafe中的方法。
public class AtomicInteger extends Number implements Serializable {
// setup to use Unsafe.compareAndSwapInt for updates
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; // volatile保证内存可见性
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
}在 JDK 9 以后,官方推荐使用 VarHandle 逐步替代直接使用 Unsafe,其底层原理依然是 CAS。
硬件实现
C++实现
在x86平台上,虚拟机相关代码会调用系统cmpxchg(Compare and Exchange)指令。hotspot/src/os_cpu/linux_x86/vm/atomic_linux_x86.inline.hpp:
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::is_MP(); // 是否为多核处理器(Multi-Processor)
__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;
}汇编实现
在 x86 架构下,最终的汇编指令为
lock cmpxchg [edx], ecxcmpxchg是CPU原生提供的比较并交换指令。
对于多核CPU,会在指令前加上lock。
lock的作用:
- 早期CPU:锁住总线,令其他CPU核心都无法访问内存。
- 现代CPU:通过MESI缓存一致性协议锁缓存行,其他核心访问该缓存行会被阻塞。
- 内存屏障:防止指令前后的读写指令重排序。
CAS的三大问题
ABA问题
线程1读取变量为A,进行处理,此时线程2将变量修改为B又修改回A,当线程1进行CAS时,变量仍然为A,于是成功执行。但变量已经经过修改,可能不符合原来业务的语义,在指针引用、链表和栈相关结构中可能出现问题。
解决方案:
- 版本号机制:每次更新时不仅比较值,还要比较版本号。
AtomicStampedReference:同时维护引用和整数版本号,同时对比值与整数版本号。AtomicMarkableReference:与AtomicStampedReference相似,但版本号改为一个boolean标记。
// 版本号:不仅比值,还比戳
AtomicStampedReference<Integer> asr = new AtomicStampedReference<>(100, 0); // (初始值, 版本号)
Integer ref = asr.getReference(); // 获取引用
int stamp = asr.getStamp(); // 获取版本号
asr.compareAndSet(100, 101, stamp, stamp + 1); // (旧值, 新值, 旧版本号, 新版本号)自旋时间过长导致CPU占用率高
在高并发竞争激烈时,大量线程不断do-while重试,占用大量CPU资源。
解决方案:
- 自适应自旋:重试一定次数后暂停,或使用
Thread.yield()和指数退避。 - 空间换时间:使用LongAdder。
只能保证单个共享变量的原子操作
CAS只针对单个内存位置,不支持多个变量。
解决方案:
- 封装对象:将多个变量封装进一个类中,使用
AtomicReference对整个对象进行CAS。 - 使用锁:使用ReentrantLock或synchronized保护多变量的临界区。