AQS独占锁如何保障线程调度公平性,有效分析其出队顺序以缓解惊群效应?

2026-04-30 16:521阅读0评论SEO教程
  • 内容介绍
  • 相关推荐

本文共计823个文字,预计阅读时间需要4分钟。

AQS独占锁如何保障线程调度公平性,有效分析其出队顺序以缓解惊群效应?

AQS通过不依赖`notifyAll()`或`broadcast()`,而是采用严格的CLH队列的FIFO顺序,只唤醒对应节点的单个线程,从而实现惊群效应的避免。即在资源争抢时,大量线程被同时唤醒、竞争,导致大量失败退回——这在AQS中不会发生。

关键在于:unparkSuccessor(Node) 方法内部只取队列中第一个有效后继节点调用 LockSupport.unpark(),其余线程仍在 park 状态,完全不参与本次竞争。这和传统 Object.wait()/notifyAll() 的粗粒度唤醒有本质区别。

acquireQueued(Node, int) 中的自旋+阻塞组合如何防止虚假唤醒干扰公平性

线程进入队列后,并非一被 unpark 就直接执行业务,而是先自旋检查前驱是否为头节点(pred == head),仅当满足才尝试 CAS 获取 state;否则继续 park。这个双重校验机制屏蔽了以下干扰:

  • LockSupport.unpark() 提前调用导致的“空唤醒”
  • 其他线程释放锁时误唤醒非后继节点(AQS 本身不会,但 JVM 层或信号可能干扰)
  • 中断状态未清导致的非预期返回

也就是说,即使线程被意外唤醒,只要它不是队列中下一个该轮到的节点,就会立刻重新 park,不破坏 FIFO 次序。

公平锁与非公平锁在出队逻辑上是否一致

出队唤醒逻辑完全一致:都是从 head 开始,唤醒 head.next。差异只存在于入队前的“插队尝试”阶段:

  • 非公平锁的 nonfairTryAcquire(int) 会先不管队列,直接 CAS 抢 state
  • 公平锁的 tryAcquire(int) 强制调用 hasQueuedPredecessors(),确认队列为空或当前线程是首节点才尝试获取

一旦线程已入队,后续的排队、唤醒、重试行为就与公平性开关无关了——出队永远是严格的双向链表 next 指针遍历。

Node.waitStatus == CANCELLED 如何影响出队链路的健壮性

取消节点(waitStatus == 1)在线程超时、中断或显式取消等待时被标记。AQS 在 unparkSuccessor(Node)shouldParkAfterFailedAcquire(Node, Node) 中都会跳过这些节点,从 tail 向前遍历寻找第一个 waitStatus 的有效后继。

这意味着:队列不是简单地 head → next → next,而是一个带“空洞”的逻辑链表。出队唤醒过程必须容忍中间节点失效,否则可能漏唤醒或卡死。这也是为什么 AQS 不依赖物理队列长度,而靠每个 Node 的 prev/nextwaitStatus 联动维护调度连续性。

真正容易被忽略的是:cancel 操作本身不修改 next 指针,只改 waitStatus;清理工作(如 cleanQueue() 类似逻辑)是延迟且非原子的,所以出队路径必须每一步都做有效性校验。

本文共计823个文字,预计阅读时间需要4分钟。

AQS独占锁如何保障线程调度公平性,有效分析其出队顺序以缓解惊群效应?

AQS通过不依赖`notifyAll()`或`broadcast()`,而是采用严格的CLH队列的FIFO顺序,只唤醒对应节点的单个线程,从而实现惊群效应的避免。即在资源争抢时,大量线程被同时唤醒、竞争,导致大量失败退回——这在AQS中不会发生。

关键在于:unparkSuccessor(Node) 方法内部只取队列中第一个有效后继节点调用 LockSupport.unpark(),其余线程仍在 park 状态,完全不参与本次竞争。这和传统 Object.wait()/notifyAll() 的粗粒度唤醒有本质区别。

acquireQueued(Node, int) 中的自旋+阻塞组合如何防止虚假唤醒干扰公平性

线程进入队列后,并非一被 unpark 就直接执行业务,而是先自旋检查前驱是否为头节点(pred == head),仅当满足才尝试 CAS 获取 state;否则继续 park。这个双重校验机制屏蔽了以下干扰:

  • LockSupport.unpark() 提前调用导致的“空唤醒”
  • 其他线程释放锁时误唤醒非后继节点(AQS 本身不会,但 JVM 层或信号可能干扰)
  • 中断状态未清导致的非预期返回

也就是说,即使线程被意外唤醒,只要它不是队列中下一个该轮到的节点,就会立刻重新 park,不破坏 FIFO 次序。

公平锁与非公平锁在出队逻辑上是否一致

出队唤醒逻辑完全一致:都是从 head 开始,唤醒 head.next。差异只存在于入队前的“插队尝试”阶段:

  • 非公平锁的 nonfairTryAcquire(int) 会先不管队列,直接 CAS 抢 state
  • 公平锁的 tryAcquire(int) 强制调用 hasQueuedPredecessors(),确认队列为空或当前线程是首节点才尝试获取

一旦线程已入队,后续的排队、唤醒、重试行为就与公平性开关无关了——出队永远是严格的双向链表 next 指针遍历。

Node.waitStatus == CANCELLED 如何影响出队链路的健壮性

取消节点(waitStatus == 1)在线程超时、中断或显式取消等待时被标记。AQS 在 unparkSuccessor(Node)shouldParkAfterFailedAcquire(Node, Node) 中都会跳过这些节点,从 tail 向前遍历寻找第一个 waitStatus 的有效后继。

这意味着:队列不是简单地 head → next → next,而是一个带“空洞”的逻辑链表。出队唤醒过程必须容忍中间节点失效,否则可能漏唤醒或卡死。这也是为什么 AQS 不依赖物理队列长度,而靠每个 Node 的 prev/nextwaitStatus 联动维护调度连续性。

真正容易被忽略的是:cancel 操作本身不修改 next 指针,只改 waitStatus;清理工作(如 cleanQueue() 类似逻辑)是延迟且非原子的,所以出队路径必须每一步都做有效性校验。