Java的CAS机制:无锁并发控制及其高频面试题
一、什么是CAS?
CAS(Compare-And-Swap,比较并交换)是一种无锁的原子操作,用于实现多线程同步。它是现代CPU通过硬件支持的原子指令,也是许多并发工具类的底层实现原理。
CAS 操作包含三个操作数:
- 内存位置(V)
- 预期原值(A)
- 新值(B)
基本逻辑是:
如果内存位置 V 的值与预期原值 A 相匹配,那么就将该位置的值更新为新值 B;否则,不修改该值。无论哪种情况,都会返回该内存位置当前的值。
这个操作是原子性的,即执行期间不会被其他线程打断。
二、Java中的CAS实现
在 Java 中,CAS 操作主要通过 java.util.concurrent.atomic
包下的原子类实现,比如:
- AtomicInteger
- AtomicLong
- AtomicBoolean
- AtomicReference
这些类底层使用 Unsafe 类 调用了 CPU 提供的 CAS 指令(如 x86 架构下的 CMPXCHG
指令)。
示例:AtomicInteger 中的 CAS
AtomicInteger atomicInteger = new AtomicInteger(0);// 模拟 CAS 操作:如果当前值是 0,就设置为 1
boolean success = atomicInteger.compareAndSet(0, 1);
compareAndSet(expect, update)
方法就是 CAS 的体现- 它会检查当前值是否等于 expect,如果是,则更新为 update,并返回 true;否则不更新,返回 false
三、CAS 的工作原理与特点
特点:
- 乐观锁策略:假设并发冲突不常发生,先尝试更新,如果不成功则重试或不处理
- 无锁(Lock-Free):不使用传统 synchronized 或 Lock,避免了线程阻塞和上下文切换
- 原子性:CAS 操作本身是原子的,由硬件保证
- 自旋(Spin):当 CAS 失败时,通常会采用自旋(循环重试)的方式,直到成功
缺点:
- ABA 问题:一个值从 A 变成 B 又变回 A,CAS 会认为没有变化,但实际上已经变更过
- 自旋开销:在高并发场景下,如果 CAS 长时间不成功,会导致大量 CPU 空转
- 只能保证一个变量的原子操作:无法直接用于多个变量的原子更新
四、CAS 的常见应用
1. 原子类
AtomicInteger counter = new AtomicInteger(0);// 线程安全的自增
counter.incrementAndGet(); // 内部使用 CAS
2. 实现非阻塞算法
比如实现一个简单的非阻塞栈或队列,利用 CAS 来更新头尾节点。
3. AQS(AbstractQueuedSynchronizer)
Java 中很多同步工具如 ReentrantLock、CountDownLatch、Semaphore 的底层都依赖 AQS,而 AQS 的核心部分就使用了 CAS 来控制线程的排队与唤醒。
五、CAS 高频面试题
1. 什么是 CAS?它的底层原理是什么?
CAS(Compare-And-Swap)是比较并交换的缩写,是一种无锁的原子操作,用于实现多线程同步。它包含三个操作数:内存地址 V、预期原值 A 和新值 B。当且仅当 V 的值等于 A 时,才将 V 的值更新为 B,否则不修改值。整个操作是原子的,由 CPU 硬件指令保证。
在 Java 中,CAS 通过 Unsafe
类调用本地方法,最终由 CPU 的原子指令(如 x86 的 CMPXCHG)实现。
2. Java 中哪些类使用了 CAS?
Java 中的 java.util.concurrent.atomic
包下的原子类,如:
- AtomicInteger
- AtomicLong
- AtomicBoolean
- AtomicReference
- 以及它们的数组版本,如
AtomicIntegerArray
此外,JUC 中的许多同步工具(如 ReentrantLock、CountDownLatch、Semaphore)的底层实现也依赖 CAS。
3. CAS 和 synchronized 有什么区别?各自优缺点是什么?
对比维度 | CAS | synchronized |
---|---|---|
实现机制 | 无锁,基于硬件原子指令 | 有锁,基于监视器锁(Monitor) |
阻塞性 | 非阻塞,线程不会挂起 | 阻塞,未获取锁的线程会被挂起 |
线程状态 | 不会进入阻塞态,一直处于就绪或运行 | 可能进入 BLOCKED 或 WAITING 状态 |
性能 | 高并发下可能自旋消耗 CPU | 高并发下线程切换开销大 |
适用场景 | 低冲突、简单原子操作 | 复杂同步逻辑、临界区较大 |
CAS 优点: 无锁、并发性能高、避免线程上下文切换
CAS 缺点: ABA 问题、自旋可能浪费 CPU、只能保证单个变量的原子性
4. 什么是 CAS 的 ABA 问题?如何解决?
ABA 问题:
线程 1 读取内存值为 A,准备将其更新为 B;但在它执行 CAS 之前,线程 2 将值从 A 改为 B,又改回 A。此时线程 1 执行 CAS,发现当前值仍是 A,于是更新成功,但实际上中间已经发生过变更。
解决方法:
使用 带版本号的原子引用类,如 AtomicStampedReference
或 AtomicMarkableReference
,在比较值的同时也检查版本号或标记位,从而识别出值是否被更改过。
示例:
AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 0);// 更新时同时检查值和版本号
int stamp = ref.getStamp();
ref.compareAndSet(100, 101, stamp, stamp + 1);
5. CAS 自旋(循环重试)会导致什么问题?如何优化?
问题:
当多个线程同时尝试修改同一个变量,只有一个线程会成功,其他线程 CAS 失败后会不断自旋重试,如果并发很高或冲突严重,会导致大量 CPU 空转,浪费资源。
优化方式:
- 限制自旋次数,超过阈值后可以放弃或转为阻塞
- 退避策略,如随机等待一段时间再重试
- 结合锁使用,在竞争激烈时降级为锁机制
- 减少热点变量的竞争,优化数据结构和算法
6. 为什么说 CAS 是乐观锁?
因为 CAS 假设并发冲突不常发生,采取“先尝试更新,失败再处理”的策略,而不是像悲观锁那样“先加锁,再操作”。它不阻止其他线程的并发访问,而是通过原子操作和重试机制来保证正确性,因此属于乐观锁的一种实现方式。
7. CAS 能否保证多个变量的原子操作?
不能直接保证。
CAS 只能针对单个变量的原子更新。如果需要保证多个变量更新的原子性,可以采用以下方式:
- 使用一个对象封装多个变量,然后对该对象的引用进行 CAS(如
AtomicReference
) - 使用锁(synchronized 或 Lock)
- 使用
AtomicReference
结合自定义对象
六、总结
CAS 是 Java 并发编程中非常重要的基础技术,是实现高性能无锁算法和并发容器的核心。它在 Atomic
类、AQS、并发工具类中都有广泛应用。
优点: 无锁、高并发性能好、避免线程阻塞
缺点: 存在 ABA 问题、自旋可能浪费 CPU、只能保证单一变量原子性