面试中回答 CAS(Compare-And-Swap) 的底层原理和应用场景时,可以采用 “概念 + 原理 + 代码 + 场景” 的结构化方式,既清晰又有深度。以下是一个参考回答:
🗣️ 标准回答模板
概念
CAS 是一种无锁算法,用于实现多线程环境下的原子操作。它的核心思想是:先比较变量的当前值是否等于预期值,如果相等则更新为新值,否则操作失败。整个过程由 CPU 硬件保证原子性。
底层原理
- 硬件支持:CAS 依赖 CPU 的原子指令(如 x86 的
cmpxchg
指令),这些指令在硬件层面保证了比较和交换操作的不可分割性。 - Java 实现:
- 在 Java 中,
sun.misc.Unsafe
类提供了compareAndSwapInt
、compareAndSwapObject
等本地方法,直接调用 CPU 的原子指令。 AtomicInteger
、AtomicLong
等原子类通过Unsafe
实现 CAS 操作。例如:java
public final boolean compareAndSet(int expectedValue, int newValue) { return U.compareAndSwapInt(this, VALUE, expectedValue, newValue); }
其中U
是Unsafe
实例,VALUE
是变量的内存偏移量。
- 在 Java 中,
应用场景
- 原子类操作:
AtomicInteger
、AtomicBoolean
等,用于实现无锁计数器、状态标记。- 示例:
java
private AtomicInteger count = new AtomicInteger(0); public void increment() { count.incrementAndGet(); // 底层用 CAS 实现 }
- 示例:
- 并发容器:
ConcurrentHashMap
、ConcurrentLinkedQueue
等,用 CAS 替代传统锁提升性能。 - 乐观锁:数据库更新时的冲突检测(如
version
字段),或分布式锁中的SETNX
命令。 - 状态转换:确保资源只被初始化一次(如单例模式):
java
private static final AtomicReference<Resource> INSTANCE = new AtomicReference<>(); public static Resource getInstance() { if (INSTANCE.get() == null) { INSTANCE.compareAndSet(null, new Resource()); } return INSTANCE.get(); }
💡 加分回答(深入理解)
-
与锁的对比:
- 锁是悲观的,认为总会发生冲突,因此先加锁;
- CAS 是乐观的,认为冲突很少,先尝试操作,失败后重试。
因此,CAS 在低冲突场景下性能远高于锁,但高冲突时可能因频繁重试导致 CPU 耗尽。
-
ABA 问题:
- 如果变量从 A→B→A,CAS 会误认为值未变。
- 解决方案:使用
AtomicStampedReference
或AtomicMarkableReference
附加版本号或标记位。 - 用一个银行转账的案例演示如何用
AtomicStampedReference
解决 CAS 的 ABA 问题 -
🌰 案例:银行账户转账 假设账户余额从 100 元转出 50 元,再转入 50 元,余额变回 100 元。如果直接用 CAS 操作,可能会忽略中间的转账操作。 import java.util.concurrent.atomic.AtomicInteger; public class ABADemo { private static AtomicInteger balance = new AtomicInteger(100); public static void main(String[] args) throws InterruptedException { // 线程1:模拟正常转账 Thread t1 = new Thread(() -> { int expected = balance.get(); System.out.println("T1 准备转出50,预期余额: " + expected); // 模拟处理延迟 try { Thread.sleep(1000); } catch (InterruptedException e) {} if (balance.compareAndSet(expected, expected - 50)) { System.out.println("T1 转出成功,当前余额: " + balance.get()); } else { System.out.println("T1 转出失败,余额已被修改"); } }); // 线程2:模拟异常操作(先转出再转入) Thread t2 = new Thread(() -> { try { Thread.sleep(200); } catch (InterruptedException e) {} // 转出50 int expected1 = balance.get(); if (balance.compareAndSet(expected1, expected1 - 50)) { System.out.println("T2 转出50,当前余额: " + balance.get()); } // 转入50 int expected2 = balance.get(); if (balance.compareAndSet(expected2, expected2 + 50)) { System.out.println("T2 转入50,当前余额: " + balance.get()); } }); t1.start(); t2.start(); t1.join(); t2.join(); } }
可能的输出结果:
-
T1 准备转出50,预期余额: 100 T2 转出50,当前余额: 50 T2 转入50,当前余额: 100 T1 转出成功,当前余额: 50 // ❌ 出现 ABA 问题:余额被错误地扣减了两次
✅ 使用 AtomicStampedReference 解决 ABA 问题
-
import java.util.concurrent.atomic.AtomicStampedReference; public class ABASolution { // 初始余额100,版本号0 private static AtomicStampedReference<Integer> balance = new AtomicStampedReference<>(100, 0); public static void main(String[] args) throws InterruptedException { // 线程1:模拟正常转账 Thread t1 = new Thread(() -> { int[] stampHolder = new int[1]; int expected = balance.get(stampHolder); int expectedStamp = stampHolder[0]; System.out.println("T1 准备转出50,预期余额: " + expected); // 模拟处理延迟 try { Thread.sleep(1000); } catch (InterruptedException e) {} // 同时检查值和版本号 if (balance.compareAndSet(expected, expected - 50, expectedStamp, expectedStamp + 1)) { System.out.println("T1 转出成功,当前余额: " + balance.getReference()); } else { System.out.println("T1 转出失败,余额已被修改"); } }); // 线程2:模拟异常操作(先转出再转入) Thread t2 = new Thread(() -> { try { Thread.sleep(200); } catch (InterruptedException e) {} // 转出50(版本号+1) int[] stampHolder1 = new int[1]; int expected1 = balance.get(stampHolder1); int stamp1 = stampHolder1[0]; if (balance.compareAndSet(expected1, expected1 - 50, stamp1, stamp1 + 1)) { System.out.println("T2 转出50,当前余额: " + balance.getReference()); } // 转入50(版本号再+1) int[] stampHolder2 = new int[1]; int expected2 = balance.get(stampHolder2); int stamp2 = stampHolder2[0]; if (balance.compareAndSet(expected2, expected2 + 50, stamp2, stamp2 + 1)) { System.out.println("T2 转入50,当前余额: " + balance.getReference()); } }); t1.start(); t2.start(); t1.join(); t2.join(); } }
输出结果:
-
T1 准备转出50,预期余额: 100 T2 转出50,当前余额: 50 T2 转入50,当前余额: 100 T1 转出失败,余额已被修改 // ✅ 正确处理了 ABA 问题
AtomicStampedReference
维护一个 值 + 版本号 的组合: - 每次修改时,版本号递增(即使值变回原来的值,版本号也不同)
- CAS 操作需要同时匹配值和版本号,只有两者都符合预期才会成功
- 关键方法:
// 获取当前值和版本号(通过数组传递) int[] stampHolder = new int[1]; int value = reference.get(stampHolder); int stamp = stampHolder[0]; // 原子性地比较并设置(需要同时匹配值和版本号) reference.compareAndSet( expectedValue, // 预期值 newValue, // 新值 expectedStamp, // 预期版本号 newStamp // 新版本号 );
-
实际项目经验:
- 例如在分布式缓存系统中,用
AtomicInteger
统计缓存命中率; - 在状态机设计中,用
AtomicBoolean
控制服务启停; - 在数据库操作中,用 CAS 实现乐观锁防止更新冲突。
- 例如在分布式缓存系统中,用
🚫 避坑指南
- 不要混淆概念:CAS 是一种无锁算法,而
synchronized
是基于锁的实现。 - 不要忽略 ABA 问题:必须提到 ABA 问题及其解决方案。
- 结合场景回答:如果面试官追问 “CAS 和 synchronized 怎么选?”,可以回答:
“CAS 适用于读多写少、冲突少的场景(如计数器),而 synchronized 适用于写多读少、冲突频繁的场景。”