今天我们来深入探讨并发编程中一个核心且常被简单概括的概念——CAS(比较并交换,Compare-And-Swap)。
谈及线程安全,很多人的第一反应是使用锁机制,例如 synchronized 或 ReentrantLock。这确实是直观且普遍的做法。然而,还存在一种更为轻量级的思路:在不显式加锁的情况下,依然保障线程安全,这正是 CAS 技术的精妙之处。
值得注意的是,“无锁”这一说法具有一定迷惑性。CAS 并非彻底消除了锁,而是将同步的职责移交给了硬件层。本文将系统性地解析:CAS 的工作原理是什么?它如何确保线程安全?以及在实际应用中需要注意哪些问题?
一个经典案例:多线程累加为何出错?
假设我们需要统计服务器的访问次数,使用一个简单的整型变量 int count = 0;,并由多个线程同时执行 count++ 操作。
这行看似简单的代码背后隐藏着线程安全问题。count++ 并非原子操作,它实际上包含三个步骤:
- 读取变量
count 的当前值。
- 将读取的值加 1。
- 将计算结果写回
count 变量。
这三个步骤对应着 CPU 的多条指令,而操作系统调度器随时可能在任意指令之间切换线程。
举例说明:
- 线程 A 读取到
count = 5,准备执行加1操作。
- 此时发生线程切换,线程 B 开始执行,同样读取到
count = 5,将其加1后写回,此时 count 变为 6。
- 线程 A 恢复执行,它持有的旧值仍是 5,加1后再次将 6 写回变量。
最终结果是两个线程各自完成了一次增加操作,但变量值只从 5 变为 6,仅增加了一次。这就是典型的线程不安全场景。
传统解决方案:加锁
最直接的解决方式是使用锁进行同步。
synchronized void increment() {
count++;
}
通过这种方式,同一时刻仅允许一个线程进入该方法执行。即使发生线程切换,其他线程也需要等待锁被释放。
优点:逻辑清晰,可靠性高。
缺点:性能开销较大。线程阻塞、上下文切换以及用户态与内核态的切换都会带来额外的消耗。特别是在高并发但竞争并不激烈的场景下,锁本身可能成为性能瓶颈。
CAS 机制:基于“重试”的非阻塞同步
是否存在一种方法,既能避免线程排队等待,又能保证操作的安全性?答案是肯定的,这就是CAS。
CAS 的核心思想非常直观:“我准备修改这个值,但前提是它现在的值和我上次看到时一样。”
具体而言,一个 CAS 操作涉及三个参数:
- 内存地址 V:需要修改的变量。
- 期望值 A:之前读取到的旧值。
- 新值 B:希望更新成的目标值。
其执行逻辑是:
比较地址 V 处的当前值是否等于期望值 A。如果相等,则交换,将地址 V 的值更新为新值 B;否则,不执行任何操作(通常选择重试)。这个过程是由 CPU 提供的一条原子指令(例如 x86 架构下的 cmpxchg 指令)完成的,确保比较和交换两个动作作为一个不可分割的整体执行。

使用 CAS 实现安全累加
仍以 count++ 为例,如何使用 CAS 实现?
AtomicInteger count = new AtomicInteger(0);
// 线程执行逻辑:
do {
int current = count.get(); // 步骤1:读取当前值
int next = current + 1; // 步骤2:计算新值
} while (!count.compareAndSet(current, next)); // 步骤3:CAS尝试更新
- 如果在此期间没有其他线程修改
count,那么 current 与内存中的当前值一致,CAS 操作成功,next 被写入。
- 如果已有其他线程抢先修改了
count,导致 current 与内存当前值不符,则 CAS 操作失败,循环体将重新执行读取、计算并尝试交换的过程。
这个不断循环尝试的过程,就是常说的自旋(Spin)。
CAS 真的完全“无锁”吗?
我们常称 CAS 为“无锁算法”,但严格来说,它只是没有在软件层面(如Java语言级别)使用传统的锁机制。
在多核 CPU 环境下,多个核心可能同时访问同一内存地址。如果每个核心都能随意进行“读-改-写”操作,那么 CAS 的“比较并交换”过程就会受到干扰。因此,现代 CPU 在执行 CAS 指令时,会通过硬件机制来保证其原子性,主要方式有两种:
- 总线锁定:锁定整个内存总线,在此期间禁止其他 CPU 访问内存。这种方法简单但粗粒度,影响整体性能。
- 缓存锁定:利用缓存一致性协议(如 MESI 协议),只锁定目标变量所在的缓存行。这种方式效率更高,是现代 CPU 的常用实现。
因此,CAS 并非魔法,它高度依赖于底层硬件的支持。所谓“无锁”,更准确的理解是“无软件锁”,硬件层面的必要同步并未缺席。理解计算机底层原理,如 CPU缓存一致性协议与内存屏障,有助于更深刻地认识这一点。
客观看待 CAS 的优缺点
任何技术方案都是权衡的艺术,CAS 也不例外。
✅ 优势
- 非阻塞:线程不会被挂起,避免了上下文切换的开销。
- 高吞吐:在竞争程度较低的场景下,性能通常优于锁机制。
- 灵活性:可作为基础原语,用于构建更复杂的无锁数据结构,例如
ConcurrentLinkedQueue。
❌ 劣势
- ABA 问题:变量值经历了 A -> B -> A 的变化,CAS 在比较时会误以为值未发生变化。可通过
AtomicStampedReference(携带版本戳的引用)或 AtomicMarkableReference 来解决。
- 自旋开销:在高竞争场景下,大量线程可能长时间循环重试,导致 CPU 资源空转,反而降低系统性能。
- 功能局限:只能保证对单个共享变量的原子操作。如果需要同时原子性地更新多个相关联的变量,CAS 无法直接实现,仍需借助锁或其他同步机制。
选择 CAS 还是锁,需根据具体场景判断。对于“读多写少”、“冲突概率低”的场景(如计数器、状态标志位),CAS 非常合适。但对于竞争激烈的资源池管理或队列头尾指针频繁变更等场景,锁可能是更稳妥的选择。
在 Java 中的应用
自 JDK 1.5 起,java.util.concurrent.atomic 包提供了一系列原子类,如 AtomicInteger、AtomicLong、AtomicReference 等。这些类的底层实现均通过 sun.misc.Unsafe 类调用本地 CAS 指令。
例如,AtomicInteger.incrementAndGet() 方法的核心逻辑就是我们上面演示的自旋 CAS 循环。此外,像 ConcurrentHashMap 这类核心 数据库与中间件 在 JDK 8 及之后的版本中,对桶(Bucket)内节点的插入采用了 CAS + synchronized 的混合策略:首先尝试使用 CAS 更新,若失败(表明存在竞争),再针对链表头或树根节点使用 synchronized 进行加锁。这种设计巧妙地平衡了性能与安全性。
总结
CAS 并非并发编程的“银弹”,但它提供了一种更细粒度、更高性能的同步控制思路。深入理解其工作原理、优势与局限,能够帮助开发者在设计高并发系统时做出更合适的技术选型,在性能与安全性之间找到最佳平衡点。下次当有人提起“CAS是无锁的”,我们可以更全面地理解:它实质上是将同步的复杂度转移到了硬件层面。