一、前言
在前面的文章中,我们从 CAS 的核心思想、底层实现、问题解决方案,到实际应用场景,完成了一套完整的知识体系搭建。但在实际开发中,没有最优的技术,只有最适合的技术。
CAS 作为无锁并发的核心方案,并非在所有场景下都能发挥优势。本文将通过对比 CAS 与 synchronized 、 ReentrantLock 、 ThreadLocal 等主流并发方案的差异,明确各自的适用场景,并整理面试中高频的 CAS 相关问题,帮助你在理论和实践层面都能做到融会贯通。
二、合理选择并发方案的关键
在 Java 并发编程领域,解决线程安全问题的方案主要分为两大类:
- 锁机制: 如 synchronized 、 ReentrantLock,属于悲观锁范畴,核心思想是 "先加锁,再执行,避免冲突";
- 无锁机制: 如 CAS,属于乐观锁范畴,核心思想是 "先执行,冲突后重试,减少阻塞"。
除此之外,还有线程隔离机制(如 ThreadLocal),通过避免共享变量来彻底解决线程安全问题。
不同的并发方案,其设计思想、性能特点、适用场景都存在显著差异。只有充分理解它们的优缺点,才能在实际开发中做出最优选择。
对比一:CAS vs synchronized
synchronized 是 Java 原生的同步关键字,从 JDK 1.0 就存在,经过 JDK 1.6 的锁优化(偏向锁、轻量级锁、重量级锁)后,性能得到了大幅提升。它与 CAS 的核心差异体现在设计思想和底层实现上。
1.实现原理差异
| 特性 | CAS | synchronized |
| 核心思想 | 乐观锁,无锁设计,冲突后重试 | 悲观锁,锁机制设计,冲突后阻塞 |
| 底层实现 | 依赖 CPU 原子指令(如 x86 的 cmpxchg)+ Unsafe 类 | JDK 1.6 后:偏向锁 / 轻量级锁基于 CAS,重量级锁基于操作系统互斥锁(Mutex) |
| 线程状态 | 竞争时线程不会阻塞,保持运行状态(自旋) | 重量级锁竞争时线程会阻塞,进入等待队列 |
| 资源开销 | 无上下文切换开销,高并发冲突时存在 CPU 自旋开销 | 重量级锁存在上下文切换开销,低冲突时偏向锁 / 轻量级锁开销极低 |
|---|
2.性能对比
性能对比的核心结论是:低并发冲突场景下,CAS 和 synchronized(偏向锁 / 轻量级锁)性能接近;高并发冲突场景下,synchronized(重量级锁)更稳定,CAS 可能引发自旋风暴。
低并发冲突场景:
-
CAS 的自旋次数极少,几乎一次就能成功,无需阻塞线程;
-
synchronized 会优先使用偏向锁或轻量级锁,同样基于 CAS 实现,开销极低。
-
两者性能差异不大,CAS 略占优势。
高并发冲突场景:
-
CAS 的自旋次数会急剧增加,大量线程空循环占用 CPU 资源,导致 CPU 利用率飙升,甚至引发 "自旋风暴",系统吞吐量下降;
-
synchronized 会升级为重量级锁,线程竞争失败后会被阻塞,进入等待队列,虽然存在上下文切换开销,但不会占用大量 CPU 资源,系统稳定性更高。
3.适用场景
| 方案 | 适用场景 | 不适用场景 |
| CAS | 1. 并发冲突较少的简单变量操作(如计数器、序列号生成器) 2. 对性能要求极高,不允许线程阻塞的场景 | 1. 高并发冲突严重的场景 2. 复杂的多变量原子操作场景 |
| synchronized | 1. 复杂的业务逻辑同步(如多变量操作、代码块同步) 2. 高并发冲突严重的场景 3. 需要自动释放锁(避免死锁)的场景 | 1. 对性能要求极致,且并发冲突极少的简单变量操作场景 |
|---|
对比二:CAS vs ReentrantLock
ReentrantLock 是 JUC 提供的可重入锁,基于 AQS(抽象队列同步器)实现,相比 synchronized,它提供了更灵活的锁控制能力(如可中断锁、公平锁、条件变量)。它与 CAS 的关系非常密切 ------ ReentrantLock 的底层实现依赖 CAS。
1.核心关联与差异
**核心关联:**ReentrantLock 的 AQS 底层通过 AtomicInteger 的 CAS 操作来修改同步状态(state 变量),实现锁的获取与释放。例如,线程获取锁时,会通过 CAS 将 state 从 0 改为 1;释放锁时,通过 CAS 将 state 从 1 改回 0。
核心差异:
| 特性 | CAS | ReentrantLock |
| 定位 | 底层原子操作方案,无锁机制 | 高层锁工具,基于 AQS 和 CAS 实现,属于悲观锁 |
| 功能 | 仅支持单个变量的原子操作 | 支持可重入、可中断、公平锁 / 非公平锁、条件变量等高级功能 |
| 异常处理 | 无自动释放机制,异常时可能导致数据不一致 | 支持 try-finally 手动释放锁,避免死锁 |
| 使用复杂度 | 简单,直接使用原子类即可 | 复杂,需要手动加锁和释放锁 |
|---|
2.选择依据
**1)如果是简单的单个变量原子操作:**优先选择 CAS(如 AtomicInteger),无需引入锁的复杂度,性能更高。
**2)如果需要复杂的锁控制功能:**优先选择 ReentrantLock,例如:
-
需要实现公平锁(线程按排队顺序获取锁);
-
需要中断正在等待锁的线程;
-
需要使用多个条件变量(Condition)实现线程间的精准通信。
**3)如果是高并发冲突场景:**ReentrantLock 的性能优于 CAS,且稳定性更高。
对比三:CAS vs ThreadLocal
ThreadLocal 是 Java 提供的线程隔离工具,它的核心思想是**为每个线程提供独立的变量副本,避免共享变量的竞争。**它与 CAS 的设计思路完全不同,是两种互补的并发方案。
1.核心思想差异
| 特性 | CAS | ThreadLocal |
| 核心思路 | 共享变量的无锁同步,通过原子操作保证线程安全 | 变量的线程隔离,为每个线程创建独立副本,避免共享数据 |
| 共享性 | 变量是多线程共享的,所有线程操作同一个变量 | 变量是线程私有的,不同线程的变量副本互不干扰 |
| 资源消耗 | 无额外内存消耗,高并发时存在 CPU 自旋开销 | 每个线程都有一份变量副本,存在额外内存消耗 |
| 适用场景 | 需要多线程共享变量,且保证线程安全的场景 | 需要线程隔离变量,避免共享竞争的场景 |
|---|
2.典型应用场景对比
- **CAS 的典型场景:**高并发计数器。多个线程共享同一个 AtomicInteger 变量,通过 CAS 实现原子自增,保证计数准确。
- **ThreadLocal 的典型场景:**SimpleDateFormat 的线程安全问题。SimpleDateFormat 是非线程安全的,通过 ThreadLocal 为每个线程提供独立的 SimpleDateFormat 副本,避免多线程竞争。
3.组合使用场景
在实际开发中,CAS 和 ThreadLocal 可以组合使用,发挥各自的优势。例如:
分布式追踪系统:
- ThreadLocal 用于存储当前线程的追踪 ID(traceId),保证线程内的追踪 ID 一致;
- CAS 用于生成全局唯一的追踪 ID(通过 AtomicLong 自增),保证多线程下 ID 的唯一性。
三、面试高频问题解析
CAS 是 Java 并发编程中的核心知识点,也是面试中的高频考点。以下整理了面试中最常见的 CAS 相关问题,并给出详细的解答思路。
1.基础概念类
问题 1:什么是 CAS?它的工作原理是什么?
解答思路:
- **定义:**CAS(Compare and Swap)即比较并交换,是一种无锁的原子操作方案,是实现乐观锁的核心技术。
- **三要素:**内存地址 V、预期原值 A、新值 B。
- **核心逻辑:**当且仅当内存地址 V 的值等于预期原值 A 时,才将 V 的值更新为新值 B,整个操作是原子性的。
- **底层支撑:**依赖 CPU 的原子指令(如 x86 的 cmpxchg),由硬件保证原子性。
问题 2:CAS 和乐观锁、悲观锁的关系是什么?
解答思路:
- **悲观锁:**假设冲突一定会发生,先加锁再执行,如 synchronized、ReentrantLock 的重量级锁。
- **乐观锁:**假设冲突很少发生,先执行再检测冲突,冲突后重试,CAS 是乐观锁的底层实现技术。
- **关系:**CAS 是实现乐观锁的核心手段,乐观锁是一种并发设计思想,CAS 是这种思想的具体实现。
2.底层实现类
问题 3:Java 中的 CAS 是如何实现的?依赖哪些核心类?
解答思路:
- **核心依赖类:**sun.misc.Unsafe 类,它是 Java 访问底层硬件指令的 "后门"。
- 实现链路:
-
应用层:使用 AtomicInteger 等原子类;
-
工具层:原子类的方法(如 compareAndSet)调用 Unsafe 类的 CAS 方法(如 compareAndSwapInt);
-
硬件层:Unsafe 类的 CAS 方法是 native 方法,底层调用 CPU 的原子指令(如 cmpxchg)。
- **关键细节:**原子类的核心变量(如 AtomicInteger 的 value)被 volatile 修饰,保证可见性。
问题 4:Unsafe 类是什么?为什么不能直接 new 一个 Unsafe 实例?
解答思路:
- **定义:**Unsafe 是 sun.misc 包下的一个类,提供了底层的内存操作、线程调度、CAS 操作等能力,是 Java 并发编程、NIO 等核心功能的底层支撑。
- 不能直接 new 的原因:
-
构造方法是私有的,无法直接实例化;
-
静态方法 getUnsafe() 做了权限校验,只有启动类加载器(Bootstrap ClassLoader)加载的类才能调用,普通应用类调用会抛出 SecurityException。
- **获取方式:**普通应用类可以通过反射获取 Unsafe 的私有静态变量 theUnsafe。
3.问题与解决方案类
问题 5:CAS 存在哪些问题?如何解决?
**解答思路:**CAS 存在三大核心问题,对应的解决方案如下:
- ABA 问题:
-
**现象:**变量值从 A 变为 B,再变回 A,CAS 认为值未变,但实际已被修改。
- **解决方案:**使用版本号机制(AtomicStampedReference)或标记机制(AtomicMarkableReference),比较值的同时比较版本号或标记。
- 循环时间长开销大:
-
**现象:**高并发冲突时,CAS 会不断自旋重试,占用大量 CPU 资源。
-
**解决方案:**限制重试次数、自适应自旋、高并发时退化为加锁机制。
- 只能保证单个变量原子操作:
-
**现象:**CAS 无法直接保证多个变量的原子操作。
-
**解决方案:**将多个变量封装为对象(使用 AtomicReference),或结合锁机制。
问题 6:什么是 ABA 问题?如何用 AtomicStampedReference 解决?
解答思路:
- **ABA 问题的本质:**CAS 只关心变量的当前值,不关心变量的变化过程,导致值被篡改后又恢复,CAS 无法感知。
-
AtomicStampedReference 的解决思路:
-
维护一个对象引用 和一个整数版本号,每次修改变量时,版本号自增 1;
-
CAS 操作时,同时比较 引用值 和 版本号,只有两者都相等时,才执行更新操作;
-
即使值从 A 变为 B 再变回 A,版本号也会从 1 变为 2 再变为 3,CAS 会检测到版本号不一致,避免 ABA 问题。
-
4.应用与对比类
问题 7:CAS 和 synchronized 的区别是什么?各自的适用场景是什么?
解答思路:
核心区别:
-
**设计思想:**CAS 是乐观锁(无锁),synchronized 是悲观锁(JDK 1.6 后支持偏向锁 / 轻量级锁 / 重量级锁);
-
**线程状态:**CAS 竞争时线程自旋不阻塞,synchronized 重量级锁竞争时线程阻塞;
-
**性能特点:**低冲突时 CAS 性能高,高冲突时 synchronized 更稳定。
适用场景:
-
CAS 适合低并发冲突的简单变量操作(如计数器);
-
synchronized 适合复杂业务逻辑同步、高并发冲突场景。
问题 8:AtomicInteger 的 incrementAndGet 方法是如何实现的?底层原理是什么?
解答思路:
- **方法功能:**原子自增并返回新值。
- 底层实现:
-
incrementAndGet 方法调用 Unsafe 的 getAndAddInt 方法;
-
getAndAddInt 方法通过 自旋 CAS 实现:不断获取当前 value 值,尝试通过 compareAndSwapInt 将值更新为 value + 1,直到 CAS 成功;
-
核心保证:value 变量被 volatile 修饰,保证可见性;compareAndSwapInt 是原子操作,保证线程安全。
三、总结
核心知识回顾
通过本系列文章的学习,我们构建了一套完整的 CAS 知识体系:
- **基础层:**CAS 的核心思想是 "比较并交换",三要素是内存地址 V、预期原值 A、新值 B,底层依赖 CPU 原子指令和 Unsafe 类。
- **实现层:**Java 通过 AtomicInteger 等原子类暴露 CAS 能力,原子类的核心变量被 volatile 修饰,保证可见性。
- **问题层:**CAS 存在 ABA 问题、自旋开销大、只能操作单个变量三大问题,需要针对性解决方案。
- **应用层:**CAS 广泛应用于原子类、无锁并发容器(如 ConcurrentLinkedQueue)、锁优化(偏向锁 / 轻量级锁)等场景。
- **对比层:**CAS 与 synchronized、ReentrantLock、ThreadLocal 各有优劣,需根据场景选择合适的并发方案。
建议
- **优先使用 JDK 原生工具:**实际开发中,优先使用 AtomicXxx 原子类、ConcurrentLinkedQueue 等 JDK 提供的工具,避免直接使用 Unsafe 类和手动实现 CAS。
- **根据场景选择方案:**低冲突简单操作选 CAS,复杂业务逻辑选 synchronized 或 ReentrantLock,线程隔离选 ThreadLocal。
- **深入理解底层原理:**不仅要会用 CAS,还要理解其底层的 CPU 原子指令、Unsafe 类、锁优化等原理,这是提升并发编程能力的关键。