深入理解CAS:无锁编程的核心,面试必考点解析

张开发
2026/5/5 18:39:47 15 分钟阅读

分享文章

深入理解CAS:无锁编程的核心,面试必考点解析
在多线程编程中线程安全始终是绕不开的核心话题。我们常用synchronized锁来保证变量修改的安全性但锁机制会带来线程阻塞、上下文切换的开销在高并发场景下性能表现并不理想。而今天要聊的CAS作为CPU原生支持的无锁原子操作凭借轻量、高效的特性成为多线程优化的关键技术也是Java面试中高频考察的重点——从基础原理到常见问题每一个知识点都可能成为面试官的“考题”。一、什么是CAS一句话读懂核心CAS全称Compare And Swap比较并交换是CPU层面原生支持的原子操作核心作用是解决多线程环境下变量修改的安全问题无需手动加锁比synchronized更轻量、开销更低。用最通俗的话总结CAS的逻辑先判断内存中变量的当前值是否和线程预期的旧值一致如果一致就把变量替换成新值如果不一致就放弃修改重新尝试。整个过程由CPU保证原子性不会被其他线程中断这也是它安全的根本原因。二、CAS的3个核心参数完整执行流程要真正理解CAS首先要明确它的三个核心参数这是后续理解执行流程和问题的基础V内存中的当前值—— 也就是主内存中存储的目标变量的真实值所有线程共享这个值。A线程的预期旧值—— 线程从主内存中读取V后保存到自己本地内存的数值线程后续会以此为标准判断变量是否被修改。B线程要修改的新值—— 线程根据自身业务逻辑对读取到的A进行计算后想要写入主内存的新数值。结合这三个参数CAS的完整执行流程可以分为3步全程无锁靠自旋重试保证最终执行效果线程从主内存中读取目标变量的当前值V将其复制到本地内存作为自己的预期旧值A线程根据业务需求比如自增、赋值等对本地的A进行计算得到想要修改的新值B执行CPU原子指令CAS操作这一步是核心分为两个关键动作比较判断主内存中的当前值V是否等于线程本地的预期旧值A交换如果V A说明这段时间没有其他线程修改过该变量直接将主内存中的V替换为新值BCAS操作成功重试如果V ! A说明变量被其他线程修改过不执行任何交换操作线程重新从主内存读取最新的V重复上述所有步骤这个重复重试的过程就是我们常说的“自旋”。这里要重点强调自旋是CAS无锁特性的核心——不同于synchronized的“失败就阻塞”CAS失败后不会让线程进入阻塞状态而是循环重试直到操作成功这也是它在低并发场景下性能出色的原因。三、Java中CAS的核心工具Atomic原子类我们不需要手动编写CAS的底层逻辑毕竟涉及CPU指令复杂度较高Java已经为我们封装好了现成的工具——java.util.concurrent.atomic包下的原子类所有原子类的底层实现都基于CAS直接调用方法就能实现线程安全的变量操作。其中最常用的就是AtomicInteger整数原子类它提供了两个核心方法完美对应CAS的逻辑compareAndSet(int expect, int update)直接实现CAS操作参数expect是线程的预期旧值Aupdate是要修改的新值B执行后返回boolean值表示操作是否成功import java.util.concurrent.atomic.AtomicInteger; public class CASDemo { public static void main(String[] args) { // 1. 初始化原子类初始值为0V初始值0 AtomicInteger num new AtomicInteger(0); // // 2. CAS操作预期旧值A0新值B10 boolean success1 num.compareAndSet(0, 10); System.out.println(第一次修改是否成功 success1); // trueV0 A0修改为10 // 3. 再次CAS操作预期旧值A0新值B20 boolean success2 num.compareAndSet(0, 20); System.out.println(第二次修改是否成功 success2); // falseV10 ! A0不修改 // 4. 查看最终值 System.out.println(最终变量值 num.get()); // 10 } }incrementAndGet()原子自增方法底层通过CAS实现“读取-自增-写入”的原子操作避免了多线程下自增的线程安全问题比如i的非原子性。public class AtomicIncrementDemo { public static void main(String[] args) { AtomicInteger num new AtomicInteger(0); // 线程安全的i底层是CAS自旋 int result num.incrementAndGet(); System.out.println(自增后的值 result); // 1 // 底层源码简化让学生理解自旋 /* public final int incrementAndGet() { for (;;) { // 自旋一直重试直到CAS成功 int current get(); // 读取当前V int next current 1; // 计算新值B if (compareAndSet(current, next)) // CAS比较交换 return next; } } */ } }除了AtomicIntegeratomic包下还有AtomicLong、AtomicBoolean、AtomicReference等原子类用法类似本质都是基于CAS实现无锁的线程安全操作。四、CAS与synchronized对比谁更适合你的场景很多面试都会问CAS和synchronized有什么区别该怎么选择其实两者没有绝对的优劣核心是匹配场景我们用一张表格清晰对比两者的核心特性特性CASsynchronized实现方式无锁依赖CPU原子指令互斥锁通过阻塞线程保证安全线程状态不阻塞失败则自旋重试失败则阻塞线程唤醒开销大性能低并发极好高并发一般自旋消耗CPU低并发一般高并发差阻塞唤醒开销适用场景读多写少、高并发、简单变量修改如计数器写多读少、逻辑复杂、多变量同时操作如事务总结一句简单变量的线程安全操作优先用CAS原子类如果是多变量操作、复杂业务逻辑还是用synchronized更稳妥。五、CAS面试重点3个核心问题必背CAS的原理不算难但它的三个核心问题自旋消耗、原子性局限、ABA问题是面试中必考的重点尤其是ABA问题几乎是Java多线程面试的“标配考题”一定要吃透。问题1自旋消耗CPU资源「问题场景」在高并发场景下多个线程同时修改同一个变量会导致CAS频繁失败线程会一直循环自旋、重试占用大量CPU资源甚至导致CPU使用率飙升。「解决方案」限制自旋次数避免无限重试。比如Java中的LongAdder类采用“分段锁自旋”结合的方式将一个变量拆分为多个分段每个线程操作不同的分段减少自旋冲突从而降低CPU消耗。问题2只能保证单个变量的原子性「问题场景」CAS一次只能操作一个变量如果业务需求需要同时修改多个变量比如同时修改a和b要求要么都修改成功要么都失败CAS无法保证这多个变量操作的原子性。「解决方案」有两种常用方式① 用AtomicReference封装对象将多个变量封装到一个对象中通过操作对象的原子性间接实现多个变量的原子操作② 放弃CAS直接使用synchronized锁通过互斥保证多变量操作的原子性。问题3ABA问题最经典、重点讲解这是CAS最经典的问题也是面试中考察最深的点一定要结合场景理解而不是死记硬背。「问题场景」结合真实业务案例假设主内存中变量V的值是A线程1读取V后保存预期旧值A准备执行修改此时线程2先将V从A改成B之后又将V从B改回A当线程1执行CAS时发现主内存中的V还是A和自己的预期旧值一致就执行了修改操作。但实际上变量V中间被线程2修改过这种“值相同但过程被篡改”的情况在某些业务场景下会导致严重错误比如银行转账、库存扣减、订单状态修改等。「问题本质」CAS只关注“变量的当前值是否和预期旧值一致”只比较“值”不关注“变量的修改过程”无法感知变量中间是否被其他线程篡改过。「解决方案」给变量加“版本号”每次修改变量时版本号自动加1。CAS操作时不再只比较“值”而是同时比较“值版本号”只有当两者都一致时才执行交换操作。Java中对应的工具类是AtomicStampedReference它可以封装“值版本号”完美解决ABA问题。六、总结CAS的核心价值与应用边界CAS的核心价值是提供了一种“无锁”的线程安全解决方案凭借CPU原子指令保证安全性凭借自旋重试避免线程阻塞在低并发、简单变量修改场景下性能远优于synchronized。但它也有自身的局限比如自旋消耗CPU、无法保证多变量原子性、存在ABA问题这些局限决定了它不能替代锁而是和锁形成互补。对于开发者来说掌握CAS不仅能在面试中从容应对考题更能在实际开发中根据场景选择合适的线程安全方案——比如计数器、累加器等场景用Atomic原子类CAS高效实现而复杂的业务逻辑、多变量操作用synchronized或Lock锁保证安全。最后提醒CAS的底层是CPU原子指令无需深入研究CPU指令细节但一定要吃透它的执行流程、核心参数和三个经典问题这才是面试和开发的核心重点。

更多文章