CAS / AQS
什么是 CAS?
CAS 全称 Compare-And-Swap(比较并交换),是一种无锁(Lock-Free)的原子操作技术。
它的核心逻辑依赖三个操作数:内存位置(V)、预期旧值(A)和新值(B)。
执行机制:当且仅当内存位置 V 的当前值等于预期旧值 A 时,才会将 V 的值更新为新值 B。如果值不等于 A,说明在这期间有其他线程修改了它,当前操作失败,通常线程会选择自旋(循环)重试。
CAS 的原子性是由底层 CPU 硬件指令(如 x86 架构下的 cmpxchg)直接保证的。
CAS 有什么问题?
CAS 虽然高效,但也存在三个典型的缺陷:
ABA 问题
值从 A 变成 B,又变回 A,CAS 会误判为没有发生过变化(详见下文)。
CPU 开销过大(自旋耗时)
在并发冲突非常激烈的场景下,CAS 会频繁失败,导致线程一直在 while 循环中空转,白白消耗大量 CPU 资源。
只能保证单个共享变量的原子操作
原生 CAS 只能针对一个内存地址生效。如果需要保证多个变量操作的原子性,原生 CAS 无法直接做到(通常需要通过 AtomicReference 将多个变量封装成一个对象来解决)。
什么是 ABA 问题?怎么解决?
现象
假设线程 1 读取了变量当前值 A,此时线程 1 被系统挂起。线程 2 介入,将值从 A 改成了 B,紧接着又改回了 A。当线程 1 恢复执行时,发起 CAS 比较,发现内存里的值依然是 A,于是成功执行了更新。虽然数据"看起来"没变,但它的状态历程已经发生了变化。这在某些业务场景(如链表节点引用转移、带状态的账户流水)下会导致严重的 Bug。
解决方案:引入版本号(Version/Stamp)
每次修改变量时,不仅改变值,还将版本号加 1(即 A(v1) → B(v2) → A(v3))。当线程 1 带着预期值 A(v1) 尝试修改时,发现内存中已经是 A(v3),CAS 比较就会失败。
在 Java 中,可以使用 AtomicStampedReference(引入版本号戳)或 AtomicMarkableReference(引入布尔标记)来彻底解决 ABA 问题。
AtomicInteger 怎么保证线程安全?
AtomicInteger 没有使用传统的 synchronized 锁,而是依靠 CAS + volatile + Unsafe 类 的组合来保证并发安全:
volatile 关键字
其内部真正存储整数的 value 变量被声明为 volatile。这保证了内存可见性,确保一个线程修改后,其他线程能立刻读取到最新值,而不是读到缓存中的旧值。
Unsafe 类
Java 无法直接操作内存,而 Unsafe 类提供了硬件级别的原子操作(Native 方法)。
自旋 CAS
以 getAndIncrement()(自增运算)为例,它底层调用了 Unsafe.getAndAddInt()。这个方法内部使用了一个 do-while 循环,不断读取当前的 volatile 值,并尝试通过 CAS 将其加 1。如果失败就重新读取重试,直到成功为止。
什么是 AQS?
AQS 全称是 AbstractQueuedSynchronizer(抽象队列同步器),它是 Java 并发包(JUC)中的核心底层框架。你可以把它理解为一个制造并发工具的"模具",Java 开发者利用它构建了各种锁和同步组件,极大地降低了处理多线程竞争的复杂度。
AQS 的核心思想是什么?
AQS 的核心架构可以总结为:状态标识 (State) + 双向阻塞队列 (FIFO Queue) + CAS。
同步状态 (state)
AQS 内部维护了一个被 volatile 修饰的 int state 变量,用来表示当前的同步状态。比如在 ReentrantLock 中,state = 0 表示锁空闲,state > 0 表示已有线程占用了锁(且数字代表重入次数)。
FIFO 等待队列
当多个线程竞争共享资源失败时,AQS 会将这些获取不到锁的线程,封装成 Node 节点,加入到一个 CLH 变体的双向链表队列中,并使用 LockSupport.park() 将它们阻塞挂起。
安全的状态变更
当持有锁的线程释放资源时,会把 state 清零,并唤醒队列中头节点的下一个等待线程。所有对 state 以及队列头尾指针的修改,AQS 都是依靠 CAS 操作来保证原子性的。
哪些 JUC 工具基于 AQS?
JUC 包中大部分常用的同步类都是在 AQS 的基础上构建的,典型代表包括:
| 工具类 | AQS 在其中的作用 |
|---|---|
| ReentrantLock | 可重入的独占锁。利用 state 记录锁的持有和重入次数。 |
| ReentrantReadWriteLock | 读写锁。巧妙地将 32 位的 state 变量按高低 16 位拆分,高位记录读锁,低位记录写锁。 |
| Semaphore (信号量) | 共享锁。state 初始值为许可证数量,每次获取减 1,释放加 1。 |
| CountDownLatch (倒计时器) | 共享锁。state 初始值为任务数,每次 countDown 使 state 减 1,为 0 时唤醒所有等待线程。 |
| ThreadPoolExecutor.Worker | 线程池中的 Worker 内部也实现了一个简易的 AQS,用来控制线程的中断响应状态。 |