node 必须是双向链表,因入队、出队、取消等待需无锁安全完成;单向链表无法从后往前清理超时/中断节点,且 prev/next 可能为 null,须判空避免 npe。

Node 节点为什么必须是双向链表?
AQS 用 Node 构建同步队列,不是为了“看起来高级”,而是因为入队、出队、取消等待这三件事必须在无锁前提下安全完成。单向链表无法从后往前清理被中断或超时的节点——比如当前线程调用 tryAcquireNanos 超时,它得把自己从队列里摘掉,但只有前驱节点知道它在哪。
实操中容易忽略的是:Node 的 prev 和 next 字段都可能为 null,尤其在初始化头节点或并发竞争激烈时。别直接写 node.prev.thread,先判空;否则 NullPointerException 会出现在最意想不到的 cancel 流程里。
enq() 方法里为什么要自旋 + CAS 插入?
enq 是 AQS 队列插入的唯一入口,它不接受失败——必须把新节点塞进队尾。自旋不是为了“等资源”,而是因为 compareAndSetTail 可能因并发失败:两个线程同时发现 tail 是 null,都去 new Node 并 CAS,只有一个成功。
常见错误现象:enq 返回前没确保 node.prev 已正确指向原 tail,就直接操作 node.prev.waitStatus,结果读到默认值 0(而不是 Node.SIGNAL),导致唤醒丢失。
- 永远用
for(;;)包裹 CAS 操作,不要只试一次 - 插入前先设置
node.prev = t,再尝试compareAndSetTail(t, node) - 失败后重新读
tail,不能复用旧的t
为什么 unparkSuccessor 要从 tail 往前找第一个非取消节点?
唤醒后继不是“叫下一个”,而是“叫下一个有效等待者”。队列中间可能堆积一堆 waitStatus == Node.CANCELLED 的节点(比如被 interrupt() 或超时打断),它们的 thread 字段已置为 null,强行 unpark 会抛 NullPointerException。
从 tail 往前找,是因为 cancelAcquire 只保证更新自己的 next 指针,不保证前驱的 next 同时刷新——所以 tail 方向的链接更“新鲜”。如果从 head 往后遍历,可能卡在某个已取消但 next 还没被修正的节点上,彻底漏掉后续有效节点。
注意:unparkSuccessor 不负责重连链表,只负责唤醒;清理取消节点是 shouldParkAfterFailedAcquire 和 cancelAcquire 的事。
head 节点为什么始终是“虚节点”?
head 不代表任何实际等待线程,它只是个占位符,用来让 acquire 成功后能原子地把当前线程节点设为新的 head。如果 head 直接存业务线程,那么 acquire 成功那一刻就得改写 head 的 thread 字段——但这不是原子操作,且和其它线程对同一节点的 waitStatus 修改冲突。
真正容易踩的坑在于:很多调试者看到 head != null && head.thread == null 就以为队列坏了,其实这是正常态。判断是否有线程在等,应该看 head.next != null && head.next.thread != null,而不是看 head 自身。
另外,setHead 方法里会把新 head 的 thread 和 prev 置为 null,但不会动 next——这意味着 head 的 next 可能指向一个已取消的节点,所以上面说的“从 tail 往前找”才不可替代。










