深入解析Java CAS机制:从硬件原理到无锁编程实战
2026/9/7 6:30:41 网站建设 项目流程

1. 项目概述:从乐观锁到CAS

在Java多线程编程里,处理共享数据最让人头疼的就是“竞态条件”。你肯定写过这样的代码:多个线程同时去读写一个计数器,最后发现结果总是不对。传统的解决方案是synchronized关键字,它简单粗暴,直接给代码块或方法加锁,保证同一时间只有一个线程能执行。这就像只有一个卫生间的办公室,谁先抢到谁用,其他人只能在外面干等。synchronized是悲观锁,它默认每次操作都会发生冲突,所以必须先独占资源。

但很多时候,冲突并没有那么频繁。为了这点小概率事件就让所有线程排队,性能开销太大了。这时候,一种更“乐观”的思路就出现了:CAS(Compare-And-Swap,比较并交换)。它假设操作大多数时候不会冲突,所以允许多个线程同时尝试更新。更新前,它会先看看要修改的变量当前值是不是和它之前读到的“期望值”一样。如果一样,说明这段时间没被别的线程改过,那就放心地改成新值;如果不一样,说明被“捷足先登”了,那这次操作就失败,通常的选择是重试。

CAS是Java并发包(java.util.concurrent,简称JUC)的基石。像AtomicIntegerConcurrentHashMap这些高性能并发工具,底层都依赖CAS。理解CAS,不仅是理解一个原子操作,更是打开了无锁编程和高效并发设计的大门。这篇文章,我会带你从硬件原理到Java实现,再到实战中的坑,彻底搞懂CAS机制。

2. CAS机制的核心原理与硬件支持

2.1 什么是CAS?一个生活化的比喻

让我们抛开术语,想象一个场景:你和朋友合租,共用一个冰箱,里面有一瓶可乐。你们约定,谁想喝这瓶可乐,需要完成一个“CAS操作”:

  1. 查看(Compare):你先走过去,看到冰箱里可乐的当前状态是“满的,未开封”。你记下这个状态作为“期望值”。
  2. 计划(Plan):你打算把它变成“空的,已喝光”这个新状态。
  3. 执行与检查(Swap):你打开冰箱门,伸手去拿可乐的瞬间,你会再次快速确认可乐的状态是否还是“满的,未开封”。
    • 如果是:说明在你“查看”到“伸手”这个极短的时间里,没人动过可乐。你成功拿走并喝掉,把空瓶放回去,状态更新为“空的”。
    • 如果不是:比如你发现可乐已经变成了“空的”,说明在你查看之后、伸手之前,你朋友已经喝掉了。那你本次“喝可乐”的操作就失败了。

这个过程就是CAS的精髓:“我认为现在是A,如果是,我就把它改成B;如果不是A,说明有人动过了,那我就不改了。”整个“查看-确认-修改”的过程必须是原子的,不可分割。你不能在确认状态还是“满的”之后,手还没拿到就被别人抢走。

在计算机中,这个“可乐”就是内存中的一个变量(比如int i),“状态”就是它的值。CAS操作就是一条CPU指令,它保证了对一个内存位置的“读-比较-写”操作在执行时不会被其他线程打断。

2.2 硬件基石:CPU的CAS指令

CAS不是Java语言层面的魔法,它需要CPU硬件的直接支持。现代处理器(如x86架构的CMPXCHG指令)都提供了这条原子指令。当Java程序调用sun.misc.Unsafe类中的compareAndSwapIntcompareAndSwapLong等方法时,最终会通过JVM(Java虚拟机)映射到这条CPU指令上。

这条指令的执行逻辑,可以用以下伪代码来理解:

// 这是一个概念性的伪代码,并非真实实现 function CAS(memory_address, expected_value, new_value) { // 原子性地执行以下操作: current_value = *memory_address; // 读取内存当前值 if (current_value == expected_value) { *memory_address = new_value; // 如果相等,则更新 return true; // 成功 } else { return false; // 失败 } }

关键在于,读取-比较-写入这三个步骤在CPU层面是一条指令完成的,中间不会有上下文切换,从而保证了原子性。

2.3 Java中的CAS API:Unsafe类

在Java中,我们一般不直接操作CPU指令。标准库通过一个“后门”类——sun.misc.Unsafe(注意,这个类名就暗示了它的不安全性)来提供CAS等底层操作。Unsafe类提供了直接操作内存、线程挂起/恢复、CAS等一系列“不安全”但功能强大的本地方法。

我们日常编程中更常用的是java.util.concurrent.atomic包下的原子类,例如AtomicInteger。我们来看看它的核心实现:

public class AtomicInteger { private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; // value字段的内存偏移地址 static { try { // 获取value字段在AtomicInteger对象内存布局中的偏移量 valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } private volatile int value; // 实际存储值的变量,用volatile保证可见性 public final boolean compareAndSet(int expect, int update) { // 调用Unsafe的CAS方法 return unsafe.compareAndSwapInt(this, valueOffset, expect, update); } public final int incrementAndGet() { // 典型的CAS循环:失败就重试 return unsafe.getAndAddInt(this, valueOffset, 1) + 1; // getAndAddInt内部就是一个do-while循环,不断尝试CAS,直到成功 } }

可以看到,AtomicInteger.incrementAndGet()这种看似简单的自增,内部就是一个基于CAS的无锁循环。Unsafe.compareAndSwapInt方法接收四个参数:操作的对象、对象中字段的偏移量、期望值、新值。它通过偏移量精准定位到内存中具体的位置进行原子操作。

注意Unsafe类在正规的Java API中是不鼓励直接使用的,因为它绕过了JVM的内存管理和安全机制,容易导致程序崩溃或安全漏洞。我们应始终优先使用java.util.concurrent.atomic包中封装好的原子类。

3. CAS的典型应用与源码解析

理解了原理,我们看看CAS在Java并发包里的经典应用。这能让你明白为什么CAS是高性能并发的关键。

3.1 原子类(Atomic Classes)

java.util.concurrent.atomic包提供了一系列原子类,如AtomicIntegerAtomicLongAtomicReference等。它们提供了原子性的更新操作,是替代synchronized进行简单计数器、状态标志更新的首选。

AtomicInteger.getAndIncrement()为例,我们深入其实现(Java 8+的典型实现):

// Unsafe类中的方法 public final int getAndAddInt(Object o, long offset, int delta) { int v; do { v = this.getIntVolatile(o, offset); // volatile读,获取当前最新值 } while (!this.compareAndSwapInt(o, offset, v, v + delta)); // CAS失败则循环重试 return v; }

这就是经典的“CAS循环”“乐观锁重试”模式:

  1. 读取变量的当前值v
  2. 基于v计算新值v + delta
  3. 尝试用CAS将变量从v更新为v + delta
  4. 如果步骤3成功,退出循环,返回旧值v
  5. 如果步骤3失败(说明v已经不是最新值),回到步骤1,重新读取最新的当前值,再次尝试。

这个过程完全是无锁的,线程不会被挂起。在低竞争环境下,大部分线程一次CAS就能成功,效率极高。

3.2 实现无锁数据结构

CAS是构建复杂无锁(Lock-Free)或非阻塞(Non-Blocking)数据结构的核心。以ConcurrentLinkedQueue(一个无锁并发队列)为例,它的入队操作核心思想如下:

  1. 找到尾节点(tail)。
  2. 将新节点的next指针设置为null。
  3. 通过CAS,尝试将尾节点的next指针从null指向新节点。
  4. 如果CAS成功,再尝试通过CAS将队列的tail指针指向新节点(这一步允许失败,因为其他线程可能已经帮忙完成了)。

这种设计允许多个线程同时尝试入队,即使它们的CAS操作有失败,也会通过循环重试,而不会导致队列状态错误。整个过程中没有使用任何锁,线程不会因为等待锁而被阻塞,极大地提升了高并发下的吞吐量。

3.3 实现轻量级同步器

著名的AQS(AbstractQueuedSynchronizer),它是ReentrantLockCountDownLatchSemaphore等同步工具的基础。AQS内部维护了一个同步状态(state),对这个状态的修改,大量使用了CAS操作。

例如,一个线程尝试获取锁,本质上就是尝试通过CAS将state从0改为1。如果成功,则获取锁;如果失败(state已经是1),则可能进入队列等待。这个过程避免了使用重量级锁带来的内核态切换开销。

4. CAS的三大经典问题与应对策略

CAS虽好,但也不是银弹。在实际使用中,必须清醒地认识到它的局限性。

4.1 ABA问题

这是CAS最著名的问题。我们回顾一下CAS的逻辑:它只检查“当前值是否等于期望值”。如果一个变量原来是A,被一个线程改成了B,然后又被改回了A。那么对于另一个只关心“是不是A”的线程来说,它的CAS操作会成功,因为它发现当前值(A)和期望值(A)相等。

这有什么问题?对于简单的计数器,值从1变成2再变回1,可能没问题。但对于某些场景,这个“A”已经不再是原来的“A”了。一个经典的比喻是“垃圾袋”:你出门前把客厅的垃圾袋(A)放在门口,打算回来时扔掉。结果你老婆在你出门期间,把垃圾倒了,又把一个新袋子(B)套在了垃圾桶上。你回来一看,门口有个空垃圾袋(外观是A),你以为还是原来那袋垃圾,直接扔了。实际上,你已经失去了“倒掉旧垃圾”这个状态变化的感知。

解决方案:版本号/时间戳给要CAS的数据加上一个版本号(Stamp)。每次数据被修改,版本号都递增。进行CAS时,同时比较“值”和“版本号”。即使值相同,版本号不同也会导致CAS失败。在Java中,AtomicStampedReference就是为解决ABA问题而生的。它维护了一个Pair对象,包含引用和版本戳(stamp)。

AtomicStampedReference<Integer> asr = new AtomicStampedReference<>(100, 0); int[] stampHolder = new int[1]; int oldRef = asr.get(stampHolder); // 同时获取引用和版本戳 int oldStamp = stampHolder[0]; // 尝试更新,必须同时满足引用和版本戳的期望 boolean success = asr.compareAndSet(oldRef, 200, oldStamp, oldStamp + 1);

4.2 循环时间长带来的开销

高竞争的环境下,多个线程反复执行CAS操作,很容易发生大量失败和重试。线程会长时间占用CPU进行空转(Busy Spin),消耗大量的计算资源,但实际进展缓慢。这就像一群人围着一个柜台抢着办业务,每个人都要反复询问“现在轮到我了没?”,场面很热闹,但效率低下。

解决方案:自适应策略或退避

  1. 自适应自旋:JVM内部的锁优化(如synchronized升级为重量级锁之前)会采用自适应自旋。如果线程最近自旋成功过,那么下次就允许它自旋更久;如果很少成功,则可能直接放弃自旋,进行线程挂起。
  2. 手动退避(Backoff):在CAS失败后,不要立即重试,而是让线程“休息”一下(如Thread.yield()让出CPU,或短暂睡眠Thread.sleep(1)),给持有资源的线程执行完成的机会,降低竞争激烈度。LongAdder在高竞争下的优秀表现,部分就源于它采用了类似“分散热点”的思路,减少了CAS冲突。

4.3 只能保证一个共享变量的原子操作

CAS指令本身是针对一个内存地址的。如果你需要同时原子性地更新多个独立的变量,单个CAS指令就无能为力了。例如,你想把变量a从1改成2,同时把变量b从3改成4,并且要求这两个改动要么都成功,要么都不成功。

解决方案:封装对象或使用AtomicReference将多个需要同时更新的变量封装到一个不可变(Immutable)对象中。然后使用AtomicReference对这个对象引用进行CAS操作。

class Point { final int x; // 使用final,确保对象状态不可变 final int y; public Point(int x, int y) { this.x = x; this.y = y; } } AtomicReference<Point> ar = new AtomicReference<>(new Point(1, 3)); Point oldP = ar.get(); Point newP = new Point(2, 4); // 创建新的不可变对象 while (!ar.compareAndSet(oldP, newP)) { oldP = ar.get(); // 失败后获取最新的Point newP = new Point(oldP.x + 1, oldP.y + 1); // 基于最新值重新计算 }

通过这种方式,我们用一次引用变量的CAS,实现了对多个逻辑变量的原子更新。这里使用不可变对象至关重要,因为一旦对象被创建,其状态就不会改变,所有修改都通过创建新对象来完成,避免了线程间看到不一致的中间状态。

5. 实战:用CAS实现一个简单的无锁栈

理论说再多,不如动手写一个。我们来实现一个线程安全的无锁栈(Treiber Stack),这是展示CAS威力的经典示例。

import java.util.concurrent.atomic.AtomicReference; public class ConcurrentStack<E> { // 栈顶节点,使用AtomicReference保证其更新的原子性 private AtomicReference<Node<E>> top = new AtomicReference<>(); // 内部节点类 private static class Node<E> { final E item; Node<E> next; Node(E item) { this.item = item; } } /** * 入栈操作 */ public void push(E item) { Node<E> newNode = new Node<>(item); Node<E> oldTop; do { oldTop = top.get(); // 1. 读取当前栈顶 newNode.next = oldTop; // 2. 新节点指向原栈顶 } while (!top.compareAndSet(oldTop, newNode)); // 3. CAS尝试更新栈顶 // 如果CAS失败(说明oldTop已不是最新栈顶),循环重试 } /** * 出栈操作 */ public E pop() { Node<E> oldTop; Node<E> newTop; do { oldTop = top.get(); // 1. 读取当前栈顶 if (oldTop == null) { return null; // 栈为空 } newTop = oldTop.next; // 2. 新的栈顶应该是原栈顶的下一个节点 } while (!top.compareAndSet(oldTop, newTop)); // 3. CAS尝试更新栈顶 // 如果CAS失败(说明oldTop已不是最新栈顶),循环重试 return oldTop.item; } public boolean isEmpty() { return top.get() == null; } }

代码解析与实操要点:

  1. 核心思想:栈的状态完全由top引用决定。pushpop操作的本质,就是通过CAS原子地改变top的指向。
  2. push操作
    • 创建新节点newNode
    • 进入循环:读取当前topoldTop),让newNode.next指向oldTop。这样,新节点在逻辑上就被放在了栈顶。
    • 关键CAS:尝试用newNode替换top。条件是top当前的值必须还是我们刚才读到的oldTop。如果是,替换成功,操作完成;如果不是,说明有其他线程在我们之前成功修改了top(完成了入栈或出栈),我们的oldTop已经过时,需要循环重试。
  3. pop操作
    • 进入循环:读取当前topoldTop)。如果为null,栈空。
    • 确定新的栈顶newTop应为oldTop.next
    • 关键CAS:尝试用newTop替换top。条件同样是top当前的值必须还是oldTop。这确保了在我们读取oldTop之后,没有其他线程修改过栈顶。如果成功,返回oldTop.item;如果失败,重试。
  4. 内存可见性AtomicReference内部通过volatileUnsafe的CAS保证了top引用的可见性。一个线程成功执行CAS更新top后,新值会立即对其他线程可见。
  5. 这是“无锁”但不是“等待-自由”:这个栈是无锁(Lock-Free)的,因为至少有一个线程(执行成功CAS的那个)能在有限步内取得进展。但它不是等待-自由(Wait-Free),因为个别线程可能因为持续CAS失败而“饥饿”(理论上可能一直重试)。不过在实际中,这种简单结构的竞争窗口很小,性能通常很好。

实操心得:编写无锁数据结构时,画出并发修改的时序图至关重要。在纸上模拟两个线程交错执行pushpop,能帮你验证CAS条件是否正确,以及是否存在ABA问题(在这个栈实现中,ABA问题不影响正确性,因为节点引用不同,但如果你复用节点对象,就可能出问题)。

6. 性能对比与选型建议

CAS和锁(如synchronized)该如何选择?没有绝对的好坏,只有适合的场景。

我设计了一个简单的基准测试:模拟100个线程,每个线程对一个共享计数器进行10000次自增。分别使用synchronizedAtomicIntegerLongAdder来实现。

// 省略详细的JMH基准测试代码框架,展示核心逻辑 // 1. 使用synchronized private int counterSync = 0; public synchronized void incrementSync() { counterSync++; } // 2. 使用AtomicInteger private AtomicInteger counterAtomic = new AtomicInteger(0); public void incrementAtomic() { counterAtomic.incrementAndGet(); } // 3. 使用LongAdder private LongAdder counterAdder = new LongAdder(); public void incrementAdder() { counterAdder.increment(); }

测试结果分析(基于典型环境,具体数值因机器而异):

实现方式低竞争(线程数少)高竞争(线程数多)特点
synchronized性能尚可,但存在锁升级开销性能下降明显,线程阻塞严重编程简单,适用场景广,但在高竞争下,线程挂起/唤醒开销大。
AtomicInteger性能最优,直接CAS一次成功性能急剧下降,大量CPU空转无锁,低竞争下无敌。高竞争时,CAS失败重试导致CPU空转,吞吐量下降。
LongAdder性能略低于AtomicInteger(有创建Cell开销)性能最优,吞吐量稳定内部采用“分段”思想,分散竞争热点。高并发下,每个线程操作自己的Cell,最后汇总,避免了单一变量的激烈CAS竞争。

选型建议:

  1. 简单的计数器、状态标志,且竞争不激烈:优先使用AtomicIntegerAtomicLong。代码简洁,性能高。
  2. 高并发下的统计、计数场景(如QPS统计)毫不犹豫地选择LongAdder。它在高竞争下的性能优势是压倒性的。ConcurrentHashMapsize()方法在Java 8中就改用类似LongAdder的思路来维护计数。
  3. 需要明确的互斥语义,或临界区操作复杂:使用synchronizedReentrantLock。例如,你需要执行“检查账户余额-扣款-记录日志”这一系列操作,必须作为一个整体原子执行,CAS很难优雅地实现这种复杂的复合操作。
  4. 构建无锁数据结构:当你需要实现队列、栈、链表等,并且追求极限性能时,深入理解并使用CAS进行设计。

注意事项:不要陷入“无锁一定比有锁快”的误区。在低竞争或临界区代码执行时间较长的场景下,锁的代价可能远小于无数线程CAS空转的代价。性能优化的黄金法则是:先测量,再优化。使用JMH等可靠的基准测试工具来获取数据,而不是凭感觉猜测。

7. 常见问题排查与进阶思考

7.1 我的CAS操作一直在循环,程序卡住了吗?

不一定。这通常是高竞争的表现。线程在不断地读取-计算-尝试CAS,但每次尝试都发现值已被其他线程改变。从外部看,程序似乎没有进展(比如计数器增长很慢),但CPU使用率会很高。

排查与解决:

  • 使用工具监控:用jstack查看线程栈,如果看到很多线程停留在原子类的getAndAddInt方法内部的循环中,就是典型的CAS重试。
  • 降低竞争:考虑是否能用LongAdder替代。或者重新设计数据结构和算法,减少对单一热点变量的争用(例如,使用线程本地存储暂存结果,定期合并)。
  • 引入退避:在CAS失败后,增加一个短暂的、随机的延迟(如Thread.yield()LockSupport.parkNanos(1)),这能显著降低竞争激烈度,提升整体吞吐量。

7.2AtomicIntegervolatile关键字有什么区别?

这是一个核心概念问题。

  • volatile:解决的是可见性有序性问题。它保证对一个变量的写操作,能立即被其他线程看到,并且禁止指令重排序。但它不保证原子性i++这种“读-改-写”操作,即使ivolatile的,在多线程下依然会出错。
  • AtomicInteger:利用volatile(其内部valuevolatile的)保证可见性,同时利用CAS来保证incrementAndGet()这类复合操作的原子性。它是volatile+ CAS的组合拳。

简单说,volatile告诉你变量最新的值是什么,但不管你怎么改它;AtomicInteger不仅告诉你最新值,还能帮你安全地修改它。

7.3 既然有LongAdderAtomicLong还有用吗?

当然有用。LongAdderAtomicLong的API语义有细微差别。

  • AtomicLong提供的get()set()compareAndSet()等操作是精确的、强一致的。你任何时候调用get(),都能立刻拿到当前系统内精确的计数值。
  • LongAdder为了性能,牺牲了实时一致性。它的sum()方法需要累加所有Cell的值,在累加过程中可能有其他线程在修改Cell,所以sum()返回的结果是一个没有并发修改瞬间的近似值,不适合用于需要精确同步控制的场景(如序列号生成器)。

选择依据:如果需要高精度、实时可见的计数器(如控制全局唯一的ID生成),用AtomicLong。如果只是做统计(如统计请求次数),允许最终一致性,并且追求高并发吞吐,用LongAdder

7.4 如何调试复杂的无锁程序?

无锁程序的Bug往往是非确定性的,难以复现。调试时:

  1. 强化不变式检查:在代码的关键位置,插入断言(assert),验证数据结构的不变式(Invariant)是否始终成立。例如在无锁栈中,可以断言“从top开始遍历,不会出现环”。
  2. 压力测试:使用大量线程长时间运行测试,比小规模测试更容易暴露并发问题。
  3. 使用专业工具JCStress(Java Concurrency Stress Test)是专门测试并发正确性的工具。ThreadSanitizer等也可以帮助检测数据竞争。
  4. 形式化验证:对于极其核心的无锁算法,可以考虑使用模型检查工具进行验证,但这通常属于高级研究范畴。

我个人在实现无锁结构时,会先写一个单线程版本确保逻辑正确,然后将其替换为AtomicReference和CAS循环,最后用JCStress进行高强度、多样化的并发场景测试。一次成功的压力测试能给你带来巨大的信心。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询