
1. 从锁到无锁为什么我们需要CAS在Java多线程编程里一提到线程安全很多人的第一反应就是synchronized关键字或者ReentrantLock。没错这些锁机制是构建并发安全大厦的基石它们通过“互斥”来保证同一时刻只有一个线程能访问共享资源。但用久了你会发现锁这东西好用是好用但“重”也是真的重。每次线程进入同步块都涉及到操作系统的内核态切换、线程的挂起与唤醒这个开销在低竞争场景下显得尤为奢侈。更头疼的是一旦设计不当锁还容易带来死锁、活锁、饥饿等一系列问题。于是追求性能极致的开发者们开始思考有没有一种更轻量级的线程安全方案能不能在不加锁的情况下安全地更新一个共享变量这个问题的答案就是CAS。CAS全称Compare-And-Swap翻译过来就是“比较并交换”。它不是Java独有的概念而是一种被现代CPU广泛支持的原子指令。它的核心思想极其朴素我认为变量V现在的值应该是A如果是那我就把它改成B如果不是说明在我读取之后、准备修改之前已经有其他线程改动了它那我就放弃本次修改通常选择重试。你可以把它想象成一个乐观的版本控制。synchronized是悲观的它默认每次都会有人来抢所以先上锁把门关死。而CAS是乐观的它默认这次不会有人跟我冲突我先去改改的时候看一眼版本号也就是旧值对不对对就提交不对就拉取最新代码再试一次。这种“无锁”的思路在高并发、低冲突的场景下性能优势是碾压性的。Java从1.5版本开始引入的java.util.concurrent.atomic包其核心正是建立在CAS机制之上为我们提供了AtomicInteger、AtomicReference等强大的原子类工具。理解CAS不仅是理解这些原子类的基石更是深入理解ConcurrentHashMap、AQS等高级并发框架的必经之路。2. CAS机制的核心原理与硬件支持2.1 CAS操作的三元组VAB要理解CAS必须吃透它的三个核心操作数V内存位置需要读写的共享变量的内存地址。比如一个AtomicInteger对象内部存储value的地址。A预期原值执行操作前线程认为的变量V当前应该具有的值。这个值通常是线程之前从V中读取出来的。B新值线程希望将变量V更新成的值。CAS指令的执行逻辑是一个不可分割的原子操作“如果内存位置V的值等于预期原值A那么处理器会自动将该位置的值更新为新值B否则不做任何操作。无论是否更新处理器都会返回该内存位置的旧值。”整个比较和交换的过程在CPU层面是一条指令完成的中间不会被其他线程打断。这是CAS能够实现无锁线程安全的最根本保障。在Java中这个能力通过Unsafe类提供的本地方法Native Method暴露出来例如compareAndSwapInt、compareAndSwapLong等。2.2 硬件基石CPU如何实现原子CAS你可能会有疑问为什么一条指令就能保证原子性这依赖于现代CPU提供的特殊硬件指令。最常见的是CMPXCHG指令Compare and Exchange在x86架构中这条指令在执行前会锁住前端总线或者缓存行从而确保在执行期间其他处理器无法访问该内存地址实现了原子性。这里有一个关键点CAS的原子性是由CPU硬件指令保证的而不是软件逻辑。软件层面的“先读后比再写”三步操作如果没有硬件支持无论如何都无法保证原子性。硬件让这三步变成了一步这是所有无锁算法得以成立的前提。2.3 Java层面的抽象Unsafe类与原子类Java开发者并不直接操作CPU指令而是通过sun.misc.Unsafe这个后门类。这个类提供了类似指针操作、内存分配、CAS等底层能力。由于它过于强大和危险如其名Unsafe普通应用代码无法直接获取其实例。JDK的并发大师们利用Unsafe封装出了我们熟悉的原子类。我们以AtomicInteger的incrementAndGet()方法为例看看其内部简化实现public final int incrementAndGet() { for (;;) { // 典型的自旋重试循环 int current get(); // 获取当前值作为预期原值A int next current 1; // 计算新值B if (compareAndSet(current, next)) // 调用CAS return next; // CAS成功返回新值 // CAS失败循环重试 } }这里的compareAndSet方法内部就是调用了Unsafe.compareAndSwapInt方法。这个for(;;)循环结构是CAS编程的经典模式被称为自旋。线程不会被挂起而是在CPU上空转循环不断尝试直到成功为止。这在冲突不频繁时效率很高因为避免了上下文切换的开销但在高冲突时会白白浪费CPU资源。注意Unsafe类在JDK内部也在被逐步规范化例如在JDK 9及以上版本许多功能被迁移到了VarHandleAPI中提供了更安全、更可控的内存操作方式。但核心的CAS思想不变。3. CAS的经典应用场景与代码实战理解了原理我们来看看CAS在Java世界里大放异彩的几个地方。这能让你更直观地感受它的威力。3.1 场景一原子计数器与累加器这是最直接的场景。假设我们有一个Web服务器需要统计请求总数。用synchronized当然可以但每个请求都抢一把锁性能瓶颈显而易见。用AtomicLong则优雅得多public class RequestCounter { private final AtomicLong requestCount new AtomicLong(0); public void handleRequest() { // 处理业务逻辑... requestCount.incrementAndGet(); // 原子递增 } public long getCount() { return requestCount.get(); } }incrementAndGet()内部就是我们刚才分析的自旋CAS。在每秒数万次请求的场景下这种无锁方式的吞吐量远超锁方案。JDK 8还引入了LongAdder和DoubleAdder它们在超高并发下通过分段累加Cell数组进一步分散热点性能比AtomicLong更优原理也是基于CAS。3.2 场景二实现无锁栈Treiber Stack这是一个展示如何用CAS构建复杂数据结构的经典例子。无锁栈的核心是维护一个栈顶节点引用入栈和出栈操作都通过CAS来更新这个引用。import java.util.concurrent.atomic.AtomicReference; public class ConcurrentStackE { private static class NodeE { final E item; NodeE next; Node(E item) { this.item item; } } private final AtomicReferenceNodeE top new AtomicReference(); public void push(E item) { NodeE newNode new Node(item); NodeE oldTop; do { oldTop top.get(); // 获取当前栈顶作为预期原值A newNode.next oldTop; // 新节点指向原栈顶 } while (!top.compareAndSet(oldTop, newNode)); // CAS尝试将栈顶更新为新节点B // 如果CAS失败说明oldTop已不是最新栈顶循环重试 } public E pop() { NodeE oldTop; NodeE newTop; do { oldTop top.get(); // A if (oldTop null) return null; // 空栈 newTop oldTop.next; // B: 新栈顶是原栈顶的下一个节点 } while (!top.compareAndSet(oldTop, newTop)); // CAS return oldTop.item; } }这个栈是完全无锁且线程安全的。push和pop操作都可能失败重试但在低冲突下性能非常好。ConcurrentLinkedQueue等无锁队列的实现也采用了类似的思路。3.3 场景三乐观锁与数据库版本号控制CAS的思想不仅用于内存也广泛应用于数据库的乐观锁实现中。例如更新一条用户记录时为了避免更新丢失通常会使用一个version字段。-- 1. 查询时获取数据和当前版本号 SELECT balance, version FROM account WHERE id 1; -- 假设得到 balance100, version1 -- 2. 更新时将版本号作为CAS条件 UPDATE account SET balance 150, version version 1 WHERE id 1 AND version 1; -- 这里的 version1 就是预期原值A这条SQL语句就是一个“数据库层面的CAS”。如果执行后影响行数为0说明在查询和更新之间version字段已被其他事务修改不再是1本次更新失败业务层需要回滚或重试。这完美复现了CAS“比较并交换”的核心逻辑。4. CAS的“阿喀琉斯之踵”你必须了解的三大问题CAS并非银弹它带来了性能提升的同时也引入了三个经典难题。能否处理好这些问题是区分普通开发者和并发高手的关键。4.1 ABA问题最隐蔽的陷阱这是CAS最著名的问题。假设一个共享变量的值原来是A线程1准备将其CAS为B。在它读取A之后、执行CAS之前线程2将值从A改成了C然后又从C改回了A。此时线程1执行CAS检查发现当前值确实是预期原值A于是CAS成功将值更新为B。问题出在哪从A到A值没变但中间状态C丢失了这个变量的“历史”发生了变化。对于简单的数值这可能无关紧要。但如果这个V是一个引用指向一个对象而对象的内容可能已经改变例如一个链表的头节点被移走又添加回来但链表内容已变就会导致严重的数据不一致。解决方案版本号StampABA问题的根源在于状态缺少“版本”标识。解决思路是给状态加上一个单调递增的版本号。Java中提供了AtomicStampedReferenceV类。它不再单纯比较引用值而是同时比较一个int类型的版本戳stamp。AtomicStampedReferenceInteger atomicStampedRef new AtomicStampedReference(100, 0); int[] stampHolder new int[1]; int oldRef atomicStampedRef.get(stampHolder); // 同时获取引用和版本戳 int oldStamp stampHolder[0]; // 尝试更新必须同时匹配旧引用和旧版本戳 boolean success atomicStampedRef.compareAndSet(oldRef, 200, oldStamp, oldStamp 1);这样即使引用值从100变回100版本戳也从0变成了1CAS就会失败。AtomicMarkableReference是另一个变种它使用一个布尔值mark作为标记适用于某些状态只有两种变化的场景。4.2 循环时间长带来的开销自旋的代价如果CAS操作长时间不成功比如线程冲突非常激烈for(;;)循环会持续占用CPU这被称为“忙等待”或“自旋”。大量线程自旋会消耗巨大的CPU资源反而可能让系统性能下降。解决方案自适应自旋与升级策略自适应自旋JVM内部的锁优化如synchronized的轻量级锁和某些并发容器会采用自适应策略。例如如果最近自旋很少成功那么下次就可能直接减少自旋次数或放弃自旋改为更传统的阻塞策略。退让策略在自旋循环中可以加入Thread.yield()提示调度器让出CPU或者短暂睡眠LockSupport.parkNanos(1L)给其他线程特别是持有锁的线程执行的机会这有助于打破僵局。问题分解这是最根本的方法。像LongAdder那样将一个热点变量拆分成多个Cell让线程竞争分散到多个内存地址上从而极大降低单个CAS位置的冲突概率。4.3 只能保证一个共享变量的原子操作CAS指令本身一次只能作用于一个内存地址变量。如果你需要同时原子地更新两个、三个独立的变量单纯的CAS就无能为力了。解决方案封装与合并封装成对象将多个需要原子更新的变量封装到一个不可变对象中然后使用AtomicReference来更新这个对象引用。这要求每次更新都创建一个新的对象。class Point { final int x, y; } // 不可变类 AtomicReferencePoint atomicPoint new AtomicReference(new Point(0, 0)); Point oldP, newP; do { oldP atomicPoint.get(); newP new Point(oldP.x 1, oldP.y 2); } while (!atomicPoint.compareAndSet(oldP, newP));使用锁当逻辑过于复杂或者需要更新的变量确实太多时无锁编程的复杂度会急剧上升。此时使用一个精确范围的锁如synchronized代码块可能是更清晰、更易维护的选择。不要为了无锁而无锁代码的正确性和可维护性永远排在第一位。5. 超越基础CAS在JUC中的高级应用探秘CAS是构建Java并发包JUC的砖石。了解它在高级组件中的应用能让你对并发编程有更深的理解。5.1 AQSAbstractQueuedSynchronizer的基石ReentrantLock、CountDownLatch、Semaphore等同步器的核心都是AQS。AQS内部维护了一个state变量int类型和一个CLH队列。所有对state的修改都是通过CAS完成的。比如tryAcquire尝试获取锁本质上就是尝试用CAS将state从0改为1。AQS的排队机制也巧妙地运用了CAS。当线程获取锁失败时需要将自己加入等待队列的尾部。这个“入队”操作在并发环境下必须保证线程安全。AQS使用的是CAS来更新尾节点tail的引用确保同一时刻只有一个线程能成功将自己设为新的尾节点其他失败的线程则循环重试。这种“无锁化”的队列管理是AQS高效的关键。5.2 ConcurrentHashMap的size()方法演进在JDK 7的ConcurrentHashMap中为了获取一个近似准确的size()它采用了分段计数再求和的方式但依然不精确。在JDK 8的重构中ConcurrentHashMap的baseCount和CounterCell[]完全依赖于CAS和LongAdder的思想。当你调用putVal方法插入数据时最后一步是调用addCount()来增加计数。它首先尝试用CAS更新baseCount。如果失败说明有竞争则检查是否要创建或使用CounterCell数组将计数累加到某个Cell上。最终size()方法就是将这些分散的值求和。整个过程几乎没有全局锁争用性能极高这正是CAS思想在分布式计数上的完美体现。5.3 内存屏障与happens-beforeCAS操作不仅仅是一个原子指令它还附带强大的内存语义。在Java中一个成功的CAS操作具有volatile读和写的内存效果。这意味着写屏障在CAS更新之前的所有写操作在程序顺序上对随后成功读取该CAS位置的线程是可见的。读屏障成功CAS之后该线程能读到之前其他线程通过CAS写入的最新值。这保证了即使在弱内存模型如某些CPU架构下基于CAS构建的无锁数据结构也能正确地在线程间传递数据变更建立起可靠的happens-before关系。这是单纯volatile变量难以实现的复杂同步的基础。6. 实战避坑指南与性能调优心得纸上得来终觉浅绝知此事要躬行。下面这些是我在实际项目中使用CAS时总结的血泪经验。6.1 如何诊断高CPU消耗自旋的火焰图当你发现应用在某个高并发场景下CPU使用率异常高比如接近100%且线程状态多为RUNNABLE而非WAITING时就要警惕可能是CAS自旋导致的。排查工具jstack抓取线程栈查看大量线程是否卡在某个原子类的compareAndSet方法调用栈上。Arthas的thread -n命令可以快速查看最忙的线程在做什么。Async Profiler或火焰图这是最直观的。通过性能剖析生成火焰图你会看到CPU时间大量消耗在sun.misc.Unsafe.compareAndSwapInt或类似的自旋循环方法上形成一个很宽的“平顶山”。优化方向一旦确认就要思考这个热点变量能否被拆分业务逻辑能否降低竞争频率能否用LongAdder替代AtomicLong或者在这个场景下是否真的需要如此极致的无锁优化换用一个简单的ReentrantLock会不会更简单、整体性能更好6.2 伪共享False Sharing看不见的性能杀手这是一个极其隐蔽的性能问题。现代CPU缓存以缓存行通常64字节为单位。假设两个频繁写的原子变量AtomicLong a和AtomicLong b在内存中恰好位于同一个缓存行。线程1在CPU核心1上疯狂CAS更新a线程2在CPU核心2上疯狂CAS更新b。虽然它们操作的是不同变量但因为位于同一缓存行每次CAS成功都会导致整个缓存行失效迫使另一个核心的缓存行失效并重新从内存加载。这造成了大量的缓存一致性流量Cache Coherency Traffic严重拖慢性能。解决方案缓存行填充在Java早期可以通过在变量前后添加一些无用的长整型字段来“填充”确保一个变量独占一个缓存行。public class PaddedAtomicLong extends AtomicLong { // 假设缓存行64字节一个long是8字节。 // 前后填充7个long加上对象头大概能保证value独占一行。 public volatile long p1, p2, p3, p4, p5, p6, p7 0L; // 真正的value继承自父类 public volatile long p8, p9, p10, p11, p12, p13, p14 0L; }注意在JDK 8中sun.misc.Contended注解被引入来专门解决伪共享。JVM会在标注了此注解的字段前后自动插入填充。LongAdder内部的Cell类就使用了这个注解。在自定义高性能数据结构时这个注解非常有用但需要注意默认只在JDK内部类生效自定义类使用需要添加JVM参数-XX:-RestrictContended。6.3 设计模式CAS与状态机的结合对于复杂的状态流转CAS是实现无锁状态机的利器。例如一个连接的状态可能是IDLE、CONNECTING、CONNECTED、CLOSING、CLOSED。用synchronized控制状态转换代码会很长。用CAS则可以非常清晰private final AtomicReferenceState state new AtomicReference(State.IDLE); public boolean connect() { State current; do { current state.get(); if (current ! State.IDLE) { return false; // 当前状态不允许连接 } } while (!state.compareAndSet(State.IDLE, State.CONNECTING)); // CAS成功状态转为CONNECTING执行实际连接操作... // 连接成功后再将状态设为CONNECTED state.set(State.CONNECTED); return true; }这种模式将状态判断和状态转换原子地绑定在一起避免了在判断之后、设置之前状态被其他线程修改的竞态条件代码既安全又简洁。6.4 最后的忠告不要滥用CASCAS是高性能的利器但也是一把双刃剑。复杂度无锁算法的正确性证明非常复杂稍有不慎就会引入极难复现的并发Bug。可调试性差基于自旋的代码在出现问题时会让你在日志和断点中迷失。并非永远最快在冲突极高的场景自旋会浪费大量CPU不如阻塞等待。我的经验法则是优先使用java.util.concurrent包中现成的高质量组件如ConcurrentHashMap、LongAdder。只有当性能 profiling 证明此处确实是瓶颈且现有组件无法满足需求时才考虑自己动手基于CAS或AtomicReference等构建自定义的无锁结构。并且一定要辅以严格的压力测试和正确性验证。记住正确的、可维护的代码远比聪明的、但脆弱的代码更有价值。