谈谈 CAS 原理?

CAS(Compare and Swap,比较并交换)是一种乐观锁技术,它通过硬件层面的原子指令,在无锁的情况下实现对共享变量的线程安全更新。它的核心思想是:“我认为共享变量的当前值应该是 A,如果是,那我就把它改成 B;如果不是,就说明被别人改过了,那我就不修改,并告诉我修改失败。”

深度解析

1. 原理与机制

CAS 的操作包含三个操作数:

  • 内存位置 V:也就是你要操作的共享变量的内存地址。
  • 预期原值 A:你期望在执行操作前,这个变量应该是什么值。
  • 新值 B:你想把这个变量设置成什么新值。

执行逻辑: CAS 指令会原子的执行以下两步:

  1. 比较 V 的值是否等于 A。
  2. 如果等于,则将 V 的值更新为 B;否则,不进行任何操作。

整个 “比较 + 更新” 是一个原子操作,由 CPU 底层指令(例如 x86 架构下的 cmpxchg 指令)保证其不可分割性。

2. 代码示例:模拟 CAS 操作

虽然我们不能直接调用 CPU 指令,但 Java 的 sun.misc.Unsafe 类提供了对 CAS 的 native 方法支持。我们平时用的原子类底层就是通过它实现的。

import java.util.concurrent.atomic.AtomicInteger;

public class CASExample {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(10);
        
        // 尝试将值从 10 更新为 20
        boolean success1 = atomicInteger.compareAndSet(10, 20);
        System.out.println("第一次更新是否成功: " + success1); // 输出 true
        System.out.println("当前值: " + atomicInteger.get());   // 输出 20
        
        // 再次尝试将值从 10 更新为 30 (此时当前值是20)
        boolean success2 = atomicInteger.compareAndSet(10, 30);
        System.out.println("第二次更新是否成功: " + success2); // 输出 false
        System.out.println("当前值: " + atomicInteger.get());   // 输出 20 (值未变)
    }
}

在这个例子中,compareAndSet 方法就是 CAS 的体现。第二次操作因为预期原值 10 与内存中的当前值 20 不符,所以更新失败。

3. 对比分析:CAS vs 传统锁

特性CAS (乐观锁)synchronized (悲观锁)
核心思想 认为并发冲突少,先更新,失败再重试。 认为并发冲突多,先加锁,再操作。
线程阻塞 无阻塞,线程不会进入阻塞状态,一直在用户态自旋。 会阻塞,未抢到锁的线程会进入阻塞状态,涉及操作系统内核态切换。
性能 在低并发、短操作的场景下性能非常高。 在高并发、长操作的场景下能有效避免 CPU 空转。
风险 ABA 问题、自旋 CPU 开销大、只能保证一个共享变量。 死锁、优先级反转、上下文切换开销。

4. 常见误区与最佳实践

误区一:忽略 ABA 问题

ABA 问题是 CAS 的一个经典陷阱。假设一个变量初始值为 A,线程 1 准备执行 CAS(A -> B),但在它读取 A 之后、执行 CAS 之前,线程 2 将值从 A 改为了 B,又改回了 A。此时线程 1 执行 CAS 时,发现内存值还是 A,就会认为变量没被修改过,于是 CAS 成功。但事实上,变量已经经历了一次 A->B->A 的变动。这在某些场景下(如无锁的链表操作)可能会引发数据一致性问题。

解决方案:

使用带有版本号/时间戳的原子引用,例如 AtomicStampedReference 或 AtomicMarkableReference。它们不仅比较值,还比较一个内部的状态戳,确保 “值” 和 “版本” 都一致才算成功。

误区二:认为 CAS 能随意替代所有锁

CAS 更适合于对单个共享变量的简单更新,比如计数器、状态标志。如果操作涉及多个变量的协同修改,或者复杂的业务逻辑,使用 synchronized 或 ReentrantLock 会让代码更简单、更安全。

最佳实践:

  • 优先使用 java.util.concurrent.atomic 包下的工具类,它们已经封装好了 CAS 操作,比如 AtomicIntegerLongAdder(在超高并发下性能更好)。
  • 在自旋(循环重试 CAS)时,可以加入一定的退避策略(如 Thread.yield() 或 Thread.sleep()),避免在竞争激烈时过度占用 CPU。
posted @ 2026-03-28 11:53  DBA日记  阅读(5)  评论(0)    收藏  举报