AQS(AbstractQueuedSynchronizer)是 Java 并发包(JUC)的基石。你提到的 waitStatus 状态变更和双向 FIFO 队列的维护,正是 AQS 实现线程阻塞与唤醒的核心机制。
简单来说,AQS 使用一个 int 类型的 state 变量来表示同步状态(锁),并使用一个 **FIFO 的双向链表(CLH 队列)**来管理获取锁失败的线程。
为了回答你的问题,我将从 Node 节点状态(waitStatus) 的流转和 双向队列的维护 两个维度来拆解。
1. 核心组件:Node 与 waitStatus
当线程获取锁失败时,AQS 会将该线程封装成一个 Node 节点加入队列。每个 Node 中有一个 waitStatus 变量,用来告诉其他线程:“我现在的状态是什么,你接下来该怎么处理我”。
waitStatus 主要有以下 5 种状态:
| 状态值 | 常量 | 含义 |
|---|---|---|
| -1 | SIGNAL | (核心) 当前节点的后继节点(下一个节点)已经被挂起,或者即将被挂起。当前节点在释放锁或取消时,必须唤醒其后继节点。 |
| 1 | CANCELLED | 当前线程已被取消(通常是超时或响应中断)。一旦进入此状态,节点状态将不再改变。 |
| -2 | CONDITION | 该节点处于 Condition 条件队列中(等待条件变量,如 condition.await())。当其他线程调用 signal() 时,该节点会被转移到同步队列中。 |
| -3 | PROPAGATE | 仅在共享模式(Shared)下使用。表示共享状态的获取应当无条件地传播下去(即唤醒后续的共享节点)。 |
| 0 | INITIAL | 默认状态,新节点入队时的初始值。 |
2. 独占锁(Exclusive)模式下的状态流转与队列维护
在独占模式下(如 ReentrantLock),只有一个线程能持有锁。队列的维护主要发生在入队和出队两个时刻。
A. 获取锁失败:入队与状态修改
- 尝试获取: 线程尝试通过
tryAcquire获取锁。 - 入队: 失败后,线程会被封装成
Node,通过 尾插法(CAS 自旋) 加入到同步队列的尾部。 - 设置前驱状态(关键):
- 新节点入队后,会检查其前驱节点的状态。
- 正常情况下,新节点会使用 CAS 操作将前驱节点的
waitStatus修改为 SIGNAL (-1)。 - 含义: “老兄,我(后继)现在要睡觉了(park),你(前驱)释放锁的时候记得叫醒我。”
B. 释放锁:唤醒与出队
- 释放: 持有锁的线程调用
unlock,执行tryRelease。 - 唤醒后继: 释放成功后,会检查头节点(当前持有锁的节点,现在即将释放)的
waitStatus。 - 状态判断: 如果头节点的
waitStatus是 SIGNAL,说明有后继节点在等待。此时会调用unparkSuccessor唤醒后继节点,并将头节点的waitStatus置回 0(或者在设置新头时处理)。 - 出队: 唤醒的线程(后继节点)会尝试获取锁。获取成功后,它会将自己设置为新的头节点(
head指针指向它),原头节点失去引用,被 GC 回收。
3. 共享锁(Shared)模式下的状态流转
在共享模式下(如 Semaphore、CountDownLatch),多个线程可以同时持有锁。
A. 状态的特殊性:PROPAGATE
共享模式比独占模式多了一个 PROPAGATE (-3) 状态。
- 当一个节点处于共享模式且需要唤醒后继节点时,它不仅要唤醒直接后继,还需要确保唤醒动作能传播下去(因为可能有多个线程都能获取共享资源)。
- 在
doReleaseShared方法中,如果发现节点状态是 0,会尝试将其设置为 PROPAGATE,以确保唤醒信号能持续传递,防止唤醒丢失。
B. 队列维护逻辑
- 获取: 线程调用
acquireShared。如果tryAcquireShared返回值 >= 0,说明获取成功。 - 唤醒传播: 如果获取成功且是共享模式,当前节点会唤醒后继节点。
- 后继处理: 后继节点被唤醒后,也会尝试获取共享资源。如果获取成功,它也会继续唤醒它的后继,直到资源被耗尽(例如
Semaphore的许可用完)。
4. 特殊情况:Condition 条件队列
你提到的 waitStatus 还有一个状态是 CONDITION (-2),这涉及到了条件变量。
- 当线程调用
Condition.await()时,它会释放锁,并将自己封装成节点放入 Condition 内部维护的单向链表(条件队列) 中,此时节点状态为 CONDITION。 - 当其他线程调用
Condition.signal()时,该节点会被从条件队列转移到同步队列的尾部。 - 转移后,节点的状态会从
CONDITION变为 0(等待获取锁)。
总结
AQS 维护双向 FIFO 队列和改变状态的逻辑可以概括为以下几点:
- 入队(尾插法): 使用 CAS 确保线程安全地将节点添加到队列尾部。
- 状态依赖(SIGNAL): 线程在阻塞(park)前,必须确保其前驱节点的状态为 SIGNAL,以此建立“唤醒契约”。
- 出队(头指针移动): 只有头节点(
head)是持有锁的线程。释放锁后,该节点会唤醒后继,并将head指针指向后继节点,完成出队。 - 共享传播(PROPAGATE): 共享模式下利用 PROPAGATE 状态确保唤醒信号能传递给所有可以获取资源的节点。
这种设计非常精妙:双向链表方便通过 prev 指针进行取消操作(如中断或超时),而 next 指针则用于快速唤醒后继节点。