Java中CAS无锁并发原理深度解析
作者:梅琳marlin2026.11.07 02:16浏览量:0简介:本文从并发安全的本质问题出发,逐步剖析CAS(Compare-And-Swap)的核心原理、硬件支撑、内存语义及其在Java中的实现。读者将理解CAS为何是构建无锁并发的基石,掌握其解决原子性、重入性问题的机制,并认识ABA问题等实践陷阱,从而在Java高并发编程中更准确地运用这一关键技术。
Java中的CAS无锁并发原理深度解析
一、从”自己实现一个同步块”说起
理解CAS最直观的路径,不是去背它的定义,而是思考一个朴素的问题:如果让我自己动手实现一个同步锁,该怎么做?
在单核、顺序执行的理想世界里,这件事异常简单。我们只需要维护一个布尔变量,比如叫 locked:
// 极度简化的"锁"示意while (locked) { /* 自旋等待 */ }locked = true;// ... 临界区代码 ...locked = false;
逻辑很清晰:进入临界区前检查 locked,如果为 false 就把它置为 true,表示”我占用了”;如果已经是 true,说明别人正在执行,就循环等待。离开时再把它置回 false。
这个模型在”整个处理器严格顺序执行”的前提下是完全成立的。但现实是,现代处理器和编译器远没有这么”老实”。
二、为什么朴素方案会崩溃
问题出在两个层面:
第一,检查与赋值不是一步完成的。 if (!locked) locked = true; 在底层会被拆解为”读取—判断—写入”多条指令。两个线程完全可能同时读到 locked == false,然后都认为自己拿到了锁,双双进入临界区,互斥性荡然无存。
第二,现代CPU会乱序执行与并行执行。 处理器可能把临界区内的指令提前执行,也可能让多条指令在不同核心、不同运算单元上同时推进。编译器同样会做指令重排优化。这意味着,即便单条读写在硬件层面是原子的,整个”检查并设置”的过程也无法保证不被穿插、不被重排。
要堵住这两个漏洞,我们需要一条不可分割的复合指令:它能在一条指令内完成”读取旧值—比较—条件写入”,并自带内存屏障语义,禁止前后指令随意越过。这正是CAS的使命。
三、CAS的语义与硬件基础
CAS的全称是 Compare-And-Swap,即”比较并交换”。它的语义可以概括为:
给定内存位置 V、预期旧值 A、新值 B。当且仅当 V 的当前值等于 A 时,才将 V 更新为 B;否则什么都不做。无论成功与否,都返回 V 的旧值。
伪代码表示:
boolean compareAndSwap(V, expected, newValue) {if (V == expected) {V = newValue;return true;}return false;}
关键在于,这一整套”比较 + 交换”由硬件保证是原子的——中间不会被任何其他操作插入。主流CPU架构都提供了对应指令,例如x86的 CMPXCHG、ARM的 LDREXR/STREXR 系列。这些指令通常还隐含着内存屏障效果,确保操作前后内存访问的顺序不会被破坏。
四、一个被忽视的”退化版本”:Swap
有意思的是,在前面那个简陋的布尔锁场景里,我们其实并不需要Compare。
回想一下:无论 locked 原来是 true 还是 false,执行完之后我们总是希望它变成 true。既然如此,何必先比较?我们只需要”设置新值并返回旧值“这一原子操作就够了——这就是 Swap(或称 Get-And-Set)。
// 语义:原子的"取值并写入"old = getAndSet(true);if (old == false) {// 之前没人占有,我拿到了锁}
可见,在这个场景中,完整的CAS其实退化成了一个更简单的原子交换。这也提醒我们:CAS并非总是必需的,选对原子原语能减少不必要的复杂度。
五、真正需要CAS完整版的场景:可重入
那什么时候非用CAS不可?答案藏在线程重入这一特性里。
Java的同步块(以及许多语言中的锁)都支持重入:同一个线程可以多次进入同一把锁保护的临界区,这在递归调用、嵌套调用中非常常见。
问题来了:如果只用布尔变量,同一线程第二次进入时,会看到 locked == true,于是误以为”别人正在执行”,把自己挡在门外——直接死锁。
要区分”是别人占着”还是”是我自己占着”,就得记录当前持有者是谁。于是布尔变量被替换成能保存线程标识(如线程ID或引用)的变量:
// 概念示意:可重入锁的CAS实现思路if (owner == currentThread) {holdCount++; // 自己重入,计数加一} else if (owner == null) {// 尝试用CAS把owner从null改成本线程if (compareAndSwap(owner, null, currentThread)) {holdCount = 1;}}
这里的判断条件变成了”预期旧值是否为我所期望的那个“——只有当 owner 恰好是 null 时,才能把自己写进去。这正是完整CAS的用武之地:预期值不再是无关紧要的,它是决定能否更新的核心条件。
六、Java中的落地:Unsafe与原子类
在Java生态中,CAS能力由底层能力暴露给上层。sun.misc.Unsafe(及后续的 VarHandle)提供了 compareAndSwapInt、compareAndSwapObject 等方法,直接映射到底层CPU的CAS指令。
基于它,java.util.concurrent.atomic 包构建了一整套无锁工具,例如:
AtomicInteger counter = new AtomicInteger(0);// 无锁自增的典型写法int oldValue, newValue;do {oldValue = counter.get();newValue = oldValue + 1;} while (!counter.compareAndSet(oldValue, newValue));
这个”读取—计算—CAS重试”的循环,就是无锁编程的经典范式(常称为CAS自旋)。如果CAS因竞争失败,说明有别的线程改过值,那就重新读取、重新计算、再试一次。整个过程没有任何线程被阻塞,吞吐量在低到中等竞争下通常优于互斥锁。
七、不可忽视的实践陷阱
CAS并非银弹,使用时必须警惕几个问题:
- ABA问题:某值从A变成B又变回A,CAS会误认为”从未变过”。在只关心数值的场景(如计数)无伤大雅,但在依赖对象状态、链表指针等场景可能致命。常见对策是引入版本号,如
AtomicStampedReference。 - 自旋开销:高竞争下大量线程反复重试,会空耗CPU。此时反而应考虑退化为锁或采用分段、限流等策略。
- 只能保护单个变量:CAS一次只能原子更新一个内存位置。若需多变量协同的原子性,通常要借助不可变对象整体替换,或改用互斥手段。
八、小结
CAS的本质,是把”读取—判断—写入”这一复合操作,交由硬件以原子方式完成,并附带内存屏障以保证顺序语义。它之所以必要,是因为现代处理器会乱序、并行执行,而朴素的检查与赋值无法应对;它对可重入等场景之所以不可或缺,是因为此时”预期值”成为更新的先决条件,退化的Swap不再够用。
理解了这一点,再看Java中那些以 Atomic 开头、以CAS自旋为核心的并发工具,就不再是黑盒——它们都是在同一个硬件原语之上,搭建起来的无锁大厦。掌握CAS,是迈向真正高并发编程的关键一步。

登录后可评论,请前往 登录 或 注册