Java中 AQS 的 enq 方法借助 CAS 自旋死循环把节点插入队列尾部的原理

作者:袖梨 2026-07-14
enq方法的核心作用是确保节点一定入队成功,它通过CAS加自旋死循环的确定性策略解决高并发竞争问题:先用CAS创建虚拟头节点(若队列为空),再CAS更新tail并手动设置prev/next指针,保障双向链表结构完整且唤醒逻辑统一。

enq 方法的核心作用是确保节点**一定入队成功**,它不依赖“运气”,而是用确定性策略解决高并发下的竞争问题。当 addWaiter 的快速路径失败(比如 tail 正被其他线程修改),就交由 enq 接手——它通过 CAS + 自旋死循环,把插入动作变成一个“必须完成”的原子过程。

为什么需要自旋死循环

因为多线程环境下,tail 可能随时被其他线程更新。单纯一次 CAS 尝试很可能失败:当前读到的 tail 已过期,CAS 比较时发现预期值不匹配,操作就被拒绝。enq 不放弃,而是不断重试,直到成功为止。

  • CAS 是非阻塞的,失败不会挂起线程,只返回 false
  • 自旋意味着线程仍在运行,持续尝试,避免上下文切换开销
  • 死循环不是无意义空转——每次迭代都重新读取最新 tail,再做一次 CAS

CAS 插入的两步关键动作

enq 内部不是直接对 tail 做 CAS,而是分两个阶段保障链表结构完整:

  • 先确保 head 存在:如果队列为空(head == null),就用 CAS 创建一个虚拟头节点(dummy head)。这个节点不关联任何线程,只起占位和引导作用
  • 再把新节点拼到 tail 后面:用 CAS 将新节点设为新的 tail;成功后,再把原 tail 的 next 指针指向新节点

这两步缺一不可。虚拟头节点让后续唤醒逻辑统一(总是从 head.next 开始找第一个有效等待者),而 CAS 更新 tail + 手动设置 prev/next,则保证双向链表的前后引用始终一致。

立即学习“Java免费学习笔记(深入)”;

为什么用虚拟头节点而不是直接让第一个等待线程当 head

这是 AQS 的精巧设计之一:

  • head 永远指向“已获取锁”或“正在执行”的节点,而非“等待中”的第一个线程
  • 刚入队的节点永远在 tail,而 head.next 才是真正要被唤醒的下一个等待者
  • 这样在 release 操作唤醒后继时,可以直接 unpark head.next.thread,无需判断 head 是否有效或是否属于等待者
  • 也避免了在初始化队列时,因 head 和 tail 指向同一节点而引发的状态歧义

自旋的边界与实际开销

虽然叫“死循环”,但真实场景中绝大多数情况下只需 1–3 次尝试就能成功:

  • 低竞争时,enq 很少被调用;高竞争时,线程分散在不同 CPU 核心上,冲突概率仍有限
  • CAS 失败后会立即重读 volatile tail,保证看到最新状态,不浪费无效轮询
  • 比起线程阻塞再唤醒的代价,短时间自旋更轻量——这也是 AQS “先自旋、再阻塞”策略的底层依据

相关文章

精彩推荐