Java 通过源码了解CAS的原理与缺点
什么是 CAS?
CAS(Compare and Swap,比较并交换)是一种并发编程中的乐观锁机制,用于实现多线程环境下的无锁同步。其核心思想是:在更新一个值之前,先比较该值是否与预期值相等,如果相等则更新,否则不做操作。
CAS 操作包含三个核心参数:
- 内存地址 V:要操作的变量地址
- 预期值 A:期望变量当前的值
- 新值 B:要更新的新值
操作逻辑:如果内存地址 V 中的值等于预期值 A,则将其更新为 B;否则不做任何操作。整个过程是原子的,由 CPU 指令直接支持。
Java 中的 CAS 实现
1. Unsafe 类中的 CAS 方法
Unsafe 类提供了多个 CAS 相关方法,以 compareAndSwapInt 为例:
public final native boolean compareAndSwapInt(
Object var1, // 操作的对象
long var2, // 变量在对象中的偏移量(内存地址)
int var4, // 预期值 A
int var5 // 新值 B
);
- 作用:修改对象
var1中偏移量为var2的int类型变量,若当前值等于var4,则更新为var5,返回true;否则返回false。 - 底层:通过 native 方法调用 CPU 原子指令,确保操作的原子性。
进一步看 Unsafe 的 getAndAddInt 方法(JDK 源码):
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
// 1. 获取当前内存中的值 v(预期值 A)
v = this.getIntVolatile(o, offset);
// 2. 执行 CAS:若当前值仍为 v,则更新为 v + delta
// 3. 若 CAS 失败(被其他线程修改),则循环重试
} while (!this.compareAndSwapInt(o, offset, v, v + delta));
return v;
}
核心逻辑:
- 通过
getIntVolatilevolatile 语义获取当前值(保证可见性)。 - 循环执行 CAS 操作:若成功则退出,若失败(值被其他线程修改)则重新获取最新值并再次尝试。
- 这种 "循环重试" 的机制称为自旋,是 CAS 实现无锁同步的关键。
2. 原子类中的 CAS 应用
Java 的 java.util.concurrent.atomic 包(简称AIO, 是 Java 1.7 之后引入的包,是 NIO 的升级版本,提供了异步非堵塞的 IO 操作方式)下的原子类(如 AtomicInteger)就是基于 CAS 实现的。
以 AtomicInteger 的 incrementAndGet 方法(自增 1 并返回新值)为例:
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// 存储实际值,使用 volatile 保证可见性
private volatile int value;
// 获取 Unsafe 实例
private static final Unsafe unsafe = Unsafe.getUnsafe();
// value 变量在类中的内存偏移量
private static final long valueOffset;
static {
try {
// 计算 value 字段的偏移量
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) {
throw new Error(ex);
}
}
public final int incrementAndGet() {
// 调用 Unsafe 的 getAndAddInt 实现自增
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
}
CAS 的核心依赖:Unsafe 类
private static final Unsafe unsafe = Unsafe.getUnsafe();
Unsafe是 Java 提供的 "后门" 工具类,它能直接操作内存和执行底层 CAS 指令- CAS 的原子性正是通过
Unsafe类的 native 方法实现的(这些方法直接映射到 CPU 的原子指令)
CAS 操作的三个关键要素:
(1)内存地址 V:valueOffset
valueOffset记录了value字段在AtomicInteger对象中的内存偏移量- 作用:相当于 CAS 中的 "内存地址 V",告诉 CPU 要操作的变量在内存中的具体位置
(2)预期值 A:当前内存中的值
private volatile int value;
value是实际存储整数的变量,被volatile修饰- 作用:保证多线程环境下的可见性,使得线程能获取到变量的最新值(作为 CAS 的 "预期值 A")
(3)新值 B:要更新的值
public final int incrementAndGet() {
// 调用Unsafe的getAndAddInt实现自增
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
- 这里的
1就是要增加的量,最终会计算出 "新值 B"(当前值 + 1)
但其存在以下主要缺点:
-
ABA 问题当一个值从 A 被修改为 B,再改回 A 时,CAS 会误认为该值未被修改而成功执行操作,这在某些场景下可能导致数据不一致。
public class ABADemo { public static void main(String[] args) { AtomicInteger atomicInteger = new AtomicInteger(100); // 线程1:将100 -> 101 -> 100 new Thread(() -> { atomicInteger.compareAndSet(100, 101); atomicInteger.compareAndSet(101, 100); System.out.println("线程1执行完毕"); }).start(); // 线程2:尝试将100 -> 200 new Thread(() -> { try { Thread.sleep(1000); // 等待线程1完成ABA操作 } catch (InterruptedException e) { e.printStackTrace(); } boolean result = atomicInteger.compareAndSet(100, 200); System.out.println("线程2修改结果:" + result); // 输出true,误以为值未变 }).start(); } }线程 2 看到的值虽然还是 100,但这个值已经被线程 1 修改过再改回,CAS 无法感知这种中间变化,导致误判。
解决方式:使用AtomicStampedReference,通过引入 "版本号"(标志),更新时不仅检查值是否匹配,还需验证版本号是否符合预期,避免 ABA 漏洞。public class SolveABADemo { public static void main(String[] args) { // 初始化值为100,版本号为1 AtomicStampedReference<Integer> stampedRef = new AtomicStampedReference<>(100, 1); // 线程1:100(版本1) -> 101(版本2) -> 100(版本3) new Thread(() -> { int stamp = stampedRef.getStamp(); // 获取当前版本 System.out.println("线程1初始版本:" + stamp); // 第一次修改:100->101,版本+1 stampedRef.compareAndSet(100, 101, stamp, stamp + 1); stamp = stampedRef.getStamp(); // 更新版本号 // 第二次修改:101->100,版本+1 stampedRef.compareAndSet(101, 100, stamp, stamp + 1); System.out.println("线程1最终版本:" + stampedRef.getStamp()); }).start(); // 线程2:尝试用初始版本修改100->200 new Thread(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } int stamp = stampedRef.getStamp(); // 获取当前版本(实际已变为3) System.out.println("线程2获取的版本:" + stamp); // 验证值和版本号,此时版本不匹配,修改失败 boolean result = stampedRef.compareAndSet(100, 200, 1, 2); System.out.println("线程2修改结果:" + result); // 输出false }).start(); } } -
循环时间长,CPU 开销大CAS 采用自旋方式(循环重试)直到操作成功,若并发冲突频繁导致长时间重试,会持续占用 CPU 资源,降低系统性能。
public class SpinCostDemo { private static final AtomicInteger count = new AtomicInteger(0); public static void main(String[] args) { // 高并发场景下模拟大量CAS重试 Runnable task = () -> { for (int i = 0; i < 100000; i++) { count.incrementAndGet(); // 内部执行CAS自旋 } }; // 创建大量线程模拟高并发 ExecutorService executor = Executors.newFixedThreadPool(20); long start = System.currentTimeMillis(); for (int i = 0; i < 20; i++) { executor.submit(task); } executor.shutdown(); while (!executor.isTerminated()) {} long end = System.currentTimeMillis(); System.out.println("结果:" + count.get() + ",耗时:" + (end - start) + "ms"); } }在高并发下,
incrementAndGet()内部的 CAS 操作会频繁失败并重试(自旋),导致大量 CPU 资源被消耗在循环等待上。
优化思路:
1. 降低并发冲突:通过分段锁(如LongAdder的实现)
2. 限制自旋次数:超过阈值后阻塞(如ConcurrentHashMap的实现)
3. 改用其他同步机制:在冲突频繁时使用synchronized -
仅支持单个共享变量的原子操作
CAS 只能保证对一个共享变量的操作原子性,无法直接处理多个变量的原子性场景。解决方式:可通过AtomicReference将多个变量封装为一个对象进行操作,或使用synchronized等锁机制实现多变量的原子性控制。
// 将多个变量封装为对象,通过AtomicReference保证原子性 public class MultiVariableSolution { // 封装多个变量为一个对象 static class Data { int a; int b; Data(int a, int b) { this.a = a; this.b = b; } } private static final AtomicReference<Data> dataRef = new AtomicReference<>(new Data(0, 0)); // 原子更新多个变量 public static void update(int aNew, int bNew) { Data oldData; do { oldData = dataRef.get(); // 获取当前数据 Data newData = new Data(aNew, bNew); // 创建新数据 // CAS替换整个对象 } while (!dataRef.compareAndSet(oldData, new Data(aNew, bNew))); } }
总结
-
三个核心参数:
- 内存地址 V:要操作的变量在内存中的地址(如
AtomicInteger中value字段的偏移量valueOffset)。 - 预期值 A:线程认为变量当前应该的值(如
AtomicInteger中volatile修饰的value的当前值)。 - 新值 B:线程希望将变量更新为的值(如
value + 1)。
- 内存地址 V:要操作的变量在内存中的地址(如
-
操作逻辑:
- 原子性地比较内存地址 V 中的实际值与预期值 A:
- 若相等,将该值更新为 B,返回
true(操作成功)。 - 若不相等,不做任何操作,返回
false(操作失败)。
- 若相等,将该值更新为 B,返回
- 失败时通常通过自旋重试(循环重新获取最新值并再次尝试),直到成功。
- 原子性地比较内存地址 V 中的实际值与预期值 A:
-
Java 中的实现:
- 底层依赖
sun.misc.Unsafe类的 native 方法(如compareAndSwapInt),直接调用 CPU 原子指令(如 x86 的cmpxchg)。 - 典型应用:
java.util.concurrent.atomic包下的原子类(如AtomicInteger),通过 CAS 实现线程安全的自增、更新等操作。
- 底层依赖
-
主要缺点
1. ABA 问题:
现象:变量值从 A 被修改为 B 后又改回 A,CAS 会误认为值未变而成功执行更新,导致数据 不一致(如链表节点的重复引用问题)。
解决方案:使用 AtomicStampedReference,通过引入版本号(或时间戳),更新时同时验证值和版本号,确保操作的安全性。
2. 自旋开销大:
现象:CAS 依赖自旋重试机制,若并发冲突频繁(如高并发场景下大量线程竞争同一变 量),会导致线程长时间循环重试,持续占用 CPU 资源,降低系统性能。
优化思路: 限制自旋次数(如 JUC 中某些类的实现会在重试一定次数后阻塞)。 使用分段锁(如 LongAdder 将变量分段,降低冲突概率)。
3. 仅支持单个变量的原子操作:
现象:CAS 只能保证对单个共享变量的操作原子性,无法直接处理多个变量的组合原子操作 (如同时更新 a 和 b,要求两者要么都成功,要么都失败)。
解决方案: 用 AtomicReference 将多个变量封装为一个对象,通过更新对象实现组合操作的原子性。 在复杂场景下改用 synchronized 或 ReentrantLock 等锁机制。
更多推荐



所有评论(0)