Skip to content

AQS(AbstractQueuedSynchronizer)是 Java 并发包(JUC)的基石。你提到的 waitStatus 状态变更和双向 FIFO 队列的维护,正是 AQS 实现线程阻塞与唤醒的核心机制。

简单来说,AQS 使用一个 int 类型的 state 变量来表示同步状态(锁),并使用一个 **FIFO 的双向链表(CLH 队列)**来管理获取锁失败的线程。

为了回答你的问题,我将从 Node 节点状态(waitStatus) 的流转和 双向队列的维护 两个维度来拆解。

1. 核心组件:Node 与 waitStatus

当线程获取锁失败时,AQS 会将该线程封装成一个 Node 节点加入队列。每个 Node 中有一个 waitStatus 变量,用来告诉其他线程:“我现在的状态是什么,你接下来该怎么处理我”。

waitStatus 主要有以下 5 种状态:

状态值常量含义
-1SIGNAL(核心) 当前节点的后继节点(下一个节点)已经被挂起,或者即将被挂起。当前节点在释放锁或取消时,必须唤醒其后继节点。
1CANCELLED当前线程已被取消(通常是超时或响应中断)。一旦进入此状态,节点状态将不再改变。
-2CONDITION该节点处于 Condition 条件队列中(等待条件变量,如 condition.await())。当其他线程调用 signal() 时,该节点会被转移到同步队列中。
-3PROPAGATE仅在共享模式(Shared)下使用。表示共享状态的获取应当无条件地传播下去(即唤醒后续的共享节点)。
0INITIAL默认状态,新节点入队时的初始值。

2. 独占锁(Exclusive)模式下的状态流转与队列维护

在独占模式下(如 ReentrantLock),只有一个线程能持有锁。队列的维护主要发生在入队出队两个时刻。

A. 获取锁失败:入队与状态修改

  1. 尝试获取: 线程尝试通过 tryAcquire 获取锁。
  2. 入队: 失败后,线程会被封装成 Node,通过 尾插法(CAS 自旋) 加入到同步队列的尾部。
  3. 设置前驱状态(关键):
    • 新节点入队后,会检查其前驱节点的状态。
    • 正常情况下,新节点会使用 CAS 操作将前驱节点waitStatus 修改为 SIGNAL (-1)
    • 含义: “老兄,我(后继)现在要睡觉了(park),你(前驱)释放锁的时候记得叫醒我。”

B. 释放锁:唤醒与出队

  1. 释放: 持有锁的线程调用 unlock,执行 tryRelease
  2. 唤醒后继: 释放成功后,会检查头节点(当前持有锁的节点,现在即将释放)的 waitStatus
  3. 状态判断: 如果头节点的 waitStatusSIGNAL,说明有后继节点在等待。此时会调用 unparkSuccessor 唤醒后继节点,并将头节点的 waitStatus 置回 0(或者在设置新头时处理)。
  4. 出队: 唤醒的线程(后继节点)会尝试获取锁。获取成功后,它会将自己设置为新的头节点(head 指针指向它),原头节点失去引用,被 GC 回收。

3. 共享锁(Shared)模式下的状态流转

在共享模式下(如 SemaphoreCountDownLatch),多个线程可以同时持有锁。

A. 状态的特殊性:PROPAGATE

共享模式比独占模式多了一个 PROPAGATE (-3) 状态。

  • 当一个节点处于共享模式且需要唤醒后继节点时,它不仅要唤醒直接后继,还需要确保唤醒动作能传播下去(因为可能有多个线程都能获取共享资源)。
  • doReleaseShared 方法中,如果发现节点状态是 0,会尝试将其设置为 PROPAGATE,以确保唤醒信号能持续传递,防止唤醒丢失。

B. 队列维护逻辑

  1. 获取: 线程调用 acquireShared。如果 tryAcquireShared 返回值 >= 0,说明获取成功。
  2. 唤醒传播: 如果获取成功且是共享模式,当前节点会唤醒后继节点。
  3. 后继处理: 后继节点被唤醒后,也会尝试获取共享资源。如果获取成功,它也会继续唤醒它的后继,直到资源被耗尽(例如 Semaphore 的许可用完)。

4. 特殊情况:Condition 条件队列

你提到的 waitStatus 还有一个状态是 CONDITION (-2),这涉及到了条件变量。

  • 当线程调用 Condition.await() 时,它会释放锁,并将自己封装成节点放入 Condition 内部维护的单向链表(条件队列) 中,此时节点状态为 CONDITION
  • 当其他线程调用 Condition.signal() 时,该节点会被从条件队列转移到同步队列的尾部
  • 转移后,节点的状态会从 CONDITION 变为 0(等待获取锁)。

总结

AQS 维护双向 FIFO 队列和改变状态的逻辑可以概括为以下几点:

  1. 入队(尾插法): 使用 CAS 确保线程安全地将节点添加到队列尾部。
  2. 状态依赖(SIGNAL): 线程在阻塞(park)前,必须确保其前驱节点的状态为 SIGNAL,以此建立“唤醒契约”。
  3. 出队(头指针移动): 只有头节点(head)是持有锁的线程。释放锁后,该节点会唤醒后继,并将 head 指针指向后继节点,完成出队。
  4. 共享传播(PROPAGATE): 共享模式下利用 PROPAGATE 状态确保唤醒信号能传递给所有可以获取资源的节点。

这种设计非常精妙:双向链表方便通过 prev 指针进行取消操作(如中断或超时),而 next 指针则用于快速唤醒后继节点。