【大白话说Java面试题 第112题】【并发篇】第12题:AQS 中节点的入队时机有哪些?

【大白话说Java面试题 第112题】【并发篇】第12题:AQS 中节点的入队时机有哪些? 人工智能开发基于Spring AI的智能对话系统设计Java全栈实现RAG与工具调用第12题AQS 中节点的入队时机有哪些回答核心考点 AQS 的入队时机是理解其线程调度机制的关键。大厂面试不会只问有哪三种入队时机而是深入考察每种入队的 CAS 原子性保障enq的尾插法自旋、条件队列与同步队列的转移细节transferForSignal的 CAS 状态变更、以及入队过程中的并发安全问题tail指针的 ABA 风险、CANCELLED 节点的清理时机。面试官真正想判断的是你是否能从源码层面理解 AQS 队列的动态演化过程。1. 同步队列Sync Queue的入队时机1.1 时机一独占锁获取失败acquire路径当线程调用acquire(int arg)获取独占锁时如果tryAcquire返回 false线程会被封装为 Node 并入队publicfinalvoidacquire(intarg){if(!tryAcquire(arg)acquireQueued(addWaiter(Node.EXCLUSIVE),arg))selfInterrupt();}addWaiter的入队逻辑privateNodeaddWaiter(Nodemode){NodenodenewNode(Thread.currentThread(),mode);Nodepredtail;// 快速路径tail 已初始化直接 CAS 入队if(pred!null){node.prevpred;if(compareAndSetTail(pred,node)){pred.nextnode;returnnode;}}// 慢速路径tail 未初始化或 CAS 失败自旋入队enq(node);returnnode;}关键细节先设置prev再 CAStail确保即使next指针未链接也能从prev回溯快速路径 vs 慢速路径大部分情况下 tail 已初始化直接 CAS只有队列未初始化或高并发竞争时才走enq。1.2 时机二共享锁获取失败acquireShared路径publicfinalvoidacquireShared(intarg){if(tryAcquireShared(arg)0)doAcquireShared(arg);}doAcquireShared同样调用addWaiter(Node.SHARED)但模式标记为SHAREDnextWaiter指向SHARED哨兵节点。1.3 时机三可中断/超时获取失败acquireInterruptibly/tryAcquireNanospublicfinalvoidacquireInterruptibly(intarg)throwsInterruptedException{if(Thread.interrupted())thrownewInterruptedException();if(!tryAcquire(arg))doAcquireInterruptibly(arg);// 入队 可中断阻塞}与普通acquire的区别阻塞期间响应中断直接抛出InterruptedException。1.4enq方法——自旋初始化 尾插法privateNodeenq(finalNodenode){for(;;){Nodettail;if(tnull){// 队列未初始化if(compareAndSetHead(newNode()))// CAS 设置哨兵 headtailhead;}else{node.prevt;if(compareAndSetTail(t,node)){// CAS 尾插t.nextnode;returnt;}}}}关键设计哨兵 head初始 head 是一个空 Nodethreadnull不绑定任何线程只作为队列起点自旋保证原子性compareAndSetTail失败则重试直到成功ABA 安全虽然tail可能 ABA被其他线程修改又改回但prev指针保证了链表的完整性。2. 条件队列Condition Queue的入队时机2.1 时机四调用Condition.await()当线程持有锁并调用await()时会释放锁并加入条件队列publicfinalvoidawait()throwsInterruptedException{if(Thread.interrupted())thrownewInterruptedException();NodenodeaddConditionWaiter();// 加入条件队列intsavedStatefullyRelease(node);// 完全释放锁// ... 阻塞等待}addConditionWaiter的入队逻辑privateNodeaddConditionWaiter(){NodetlastWaiter;// 清理已取消的尾节点if(t!nullt.waitStatus!Node.CONDITION){unlinkCancelledWaiters();tlastWaiter;}NodenodenewNode(Thread.currentThread(),Node.CONDITION);if(tnull)firstWaiternode;elset.nextWaiternode;lastWaiternode;returnnode;}关键细节无需 CAS条件队列的入队在持有锁的情况下进行await调用前必须持有锁因此是单线程操作无需 CAS清理 CANCELLED 节点入队前检查并清理尾部的 CANCELLED 节点避免链表污染。2.2 时机五调用awaitUninterruptibly/awaitNanos/awaitUntil这些变体方法同样会调用addConditionWaiter但阻塞期间的行为不同方法中断响应超时支持await()立即抛出异常无awaitUninterruptibly()忽略中断无awaitNanos(long)中断 超时返回纳秒级超时awaitUntil(Date)中断 超时返回绝对时间超时3. 条件队列 → 同步队列的转移时机3.1 时机六调用Condition.signal()publicfinalvoidsignal(){if(!isHeldExclusively())thrownewIllegalMonitorStateException();NodefirstfirstWaiter;if(first!null)doSignal(first);}transferForSignal的转移逻辑finalbooleantransferForSignal(Nodenode){// 步骤1将 CONDITION 状态改为 0CAS 保证原子性if(!compareAndSetWaitStatus(node,Node.CONDITION,0))returnfalse;// 节点已取消// 步骤2入队到同步队列尾部Nodepenq(node);intwsp.waitStatus;// 步骤3如果前驱已取消或设置 SIGNAL 失败直接唤醒if(ws0||!compareAndSetWaitStatus(p,ws,Node.SIGNAL))LockSupport.unpark(node.thread);returntrue;}关键细节CAS 状态变更CONDITION( -2) → 0是原子操作确保节点在转移过程中不会被其他线程操作转移后不一定立即唤醒如果前驱正常且设置 SIGNAL 成功节点等待前驱释放时唤醒只有前驱异常时才立即unpark。3.2 时机七调用Condition.signalAll()doSignalAll遍历整个条件队列将所有节点逐个转移到同步队列privatevoiddoSignalAll(Nodefirst){lastWaiterfirstWaiternull;do{Nodenextfirst.nextWaiter;first.nextWaiternull;transferForSignal(first);firstnext;}while(first!null);}3.3 时机八中断导致的被动转移线程在条件队列中等待时被中断会触发transferAfterCancelledWaitprivateintcheckInterruptWhileWaiting(Nodenode){returnThread.interrupted()?(transferAfterCancelledWait(node)?THROW_IE:REINTERRUPT):0;}中断后节点会被强制转移到同步队列但此时可能尚未被signal属于提前唤醒。4. 特殊入队时机——CANCELLED 节点的清理4.1 时机九获取锁超时/中断后标记为 CANCELLED当线程在acquireQueued中因中断或超时而取消时privatevoidcancelAcquire(Nodenode){if(nodenull)return;node.threadnull;// 跳过前驱的 CANCELLED 节点Nodeprednode.prev;while(pred.waitStatus0)node.prevpredpred.prev;NodepredNextpred.next;node.waitStatusNode.CANCELLED;// 如果当前节点是 tail直接移除if(nodetailcompareAndSetTail(node,pred)){compareAndSetNext(pred,predNext,null);}else{// 否则让前驱负责清理在 shouldParkAfterFailedAcquire 中if(pred!headpred.waitStatusNode.SIGNAL||compareAndSetWaitStatus(pred,0,Node.SIGNAL)pred.thread!null){Nodenextnode.next;if(next!nullnext.waitStatus0)compareAndSetNext(pred,predNext,next);}else{unparkSuccessor(node);// 唤醒后继让它自己处理}}}清理策略尾节点直接移除CAS 设置tail pred中间节点延迟清理不立即从链表中移除而是依赖后继线程的shouldParkAfterFailedAcquire跳过 CANCELLED 节点。5. 入队时机的完整分类与对比入队时机目标队列触发条件CAS 操作线程状态变化独占锁获取失败同步队列tryAcquire返回 falsecompareAndSetTailRUNNABLE → 自旋/阻塞共享锁获取失败同步队列tryAcquireShared 0compareAndSetTailRUNNABLE → 自旋/阻塞可中断获取失败同步队列tryAcquire返回 falsecompareAndSetTailRUNNABLE → 可中断阻塞超时获取失败同步队列tryAcquire返回 falsecompareAndSetTailRUNNABLE → 限时阻塞await()条件队列调用await()且持有锁无需 CAS单线程RUNNABLE → 释放锁阻塞signal()同步队列调用signal()且持有锁compareAndSetWaitStatusenqCONDITION → 等待锁signalAll()同步队列调用signalAll()多次transferForSignalCONDITION → 等待锁中断被动转移同步队列条件队列中线程被中断transferAfterCancelledWaitCONDITION → 等待锁超时取消同步队列CANCELLEDtryAcquireNanos超时cancelAcquire阻塞 → CANCELLED6. 入队过程中的并发安全问题6.1tail的 ABA 问题时间线 T1: 线程 A 读取 tail NodeX T2: 线程 B CAS tail NodeY T3: 线程 C CAS tail NodeXNodeX 被重新入队 T4: 线程 A CAS tail NodeZ基于旧的 NodeX但 NodeX 已非原节点解决方案node.prev pred先设置即使tailABA也能通过prev指针构建完整链表。6.2next指针的可见性next指针是普通变量非 volatile依赖tail的 happens-before 保证可见性// enq 中的顺序node.prevt;// 普通写compareAndSetTail(t,node);// volatile 写 happens-before 屏障t.nextnode;// 普通写对后续读 tail 的线程可见6.3 条件队列的线程安全条件队列的入队/出队不需要 CAS因为await()要求当前线程持有锁入队是单线程操作signal()要求当前线程持有锁出队也是单线程操作。这是 AQS 设计的精妙之处——用锁保护条件队列用 CAS 保护同步队列。7. 生产环境避坑指南7.1 避免在tryAcquire中触发其他入队tryAcquire是回调方法如果内部调用其他会触发 AQS 入队的方法如另一个lock.acquire()会导致嵌套入队死锁。7.2 注意signal前必须持有锁signal()内部调用isHeldExclusively()检查未持有锁会抛出IllegalMonitorStateException。这是常见 Bug尤其在异步回调中signal。7.3 条件队列的内存泄漏如果signal被遗漏如异常分支未执行条件队列中的节点会永久等待。应使用signalAll或确保所有路径都有signal。7.4 监控队列长度ReentrantLocklocknewReentrantLock();// 入队后检查队列长度if(lock.getQueueLength()100){logger.warn(AQS queue too long: {},lock.getQueueLength());}8. 面试官追问与高分回答模板追问 1“AQS 中节点的入队时机有哪些”低分回答“竞争失败入同步队列await 入条件队列signal 转移队列。”遗漏了多种变体高分回答AQS 的入队时机可分为三大类九种情况同步队列入队4 种独占锁acquire失败、共享锁acquireShared失败、可中断acquireInterruptibly失败、限时tryAcquireNanos失败条件队列入队2 种await()、awaitUninterruptibly/awaitNanos/awaitUntil队列间转移3 种signal()转移头节点、signalAll()转移全部节点、中断导致的被动转移。此外超时/中断会导致节点被标记为 CANCELLED这也是一种特殊的状态变更。关键区别同步队列入队需要 CAS多线程竞争条件队列入队无需 CAS单线程持有锁转移时需要 CAS 变更waitStatus并调用enq。追问 2“同步队列的入队为什么需要 CAS条件队列为什么不需要”高分回答同步队列管理的是竞争锁失败的线程这些线程来自多个 CPU 核心同时尝试入队必须用 CAS 保证尾插法的原子性compareAndSetTail。条件队列管理的是调用 await() 的线程而await()的调用前提是当前线程必须持有锁。既然持有锁同一时刻只有一个线程能操作条件队列因此是单线程操作无需 CAS。这是 AQS 设计的精妙之处用锁保护条件队列简化实现用 CAS 保护同步队列支持高并发。追问 3“enq方法为什么要用哨兵 head直接让第一个线程作为 head 不行吗”高分回答哨兵 headthreadnull的空节点有两个核心作用简化边界处理acquireQueued中判断p head时尝试获取锁如果 head 绑定真实线程需要额外处理线程已释放但节点未出队的情况统一释放逻辑release时唤醒head.next如果 head 是真实线程且已退出需要特殊处理。哨兵 head 保证队列始终有头节点释放逻辑统一。另外哨兵 head 的waitStatus可以承载 SIGNAL 状态提示后继节点我释放时会唤醒你。追问 4“signal后节点一定立即被唤醒吗”高分回答不一定。transferForSignal将节点从条件队列转移到同步队列后有两种情况正常路径前驱节点waitStatus正常CAS 设置为 SIGNAL。此时节点进入同步队列等待直到前驱释放锁时唤醒它快速路径前驱已取消ws 0或设置 SIGNAL 失败直接LockSupport.unpark(node.thread)唤醒。大部分情况下走正常路径因为signal调用时通常前驱正常。但极端并发下可能走快速路径。追问 5“CANCELLED 节点为什么不立即从链表中移除”高分回答CANCELLED 节点采用延迟清理策略原因有三避免竞争cancelAcquire时可能持有锁的线程正在遍历链表如unparkSuccessor立即移除需要复杂同步简化实现依赖后继线程的shouldParkAfterFailedAcquire跳过 CANCELLED 节点将清理责任分散到多个线程尾节点快速移除如果 CANCELLED 节点是 tail可以直接 CAS 移除compareAndSetTail因为 tail 只有一个竞争者。这种设计是’空间换时间’用少量内存占用换取更简单的并发控制。追问 6“如果tryAcquire内部又调用了另一个 AQS 的acquire会发生什么”高分回答这会导致嵌套入队死锁。例如线程 A 在 Lock1 的tryAcquire中调用 Lock2.acquire()如果 Lock2 也获取失败线程 A 会入 Lock2 的同步队列并阻塞。但此时线程 A 可能持有 Lock1 的部分状态如已修改了 Lock1 的 state导致 Lock1 无法被其他线程释放形成死锁。最佳实践tryAcquire必须是’纯函数’只操作当前 AQS 的 state严禁调用其他阻塞方法、IO 或嵌套锁。9. 方案选型速查表场景入队方法队列类型是否 CAS注意事项普通获取独占锁addWaiter(EXCLUSIVE)同步队列✅先prev再 CAStail普通获取共享锁addWaiter(SHARED)同步队列✅nextWaiter指向 SHARED 哨兵可中断获取锁addWaiter(EXCLUSIVE)同步队列✅阻塞期间响应中断限时获取锁addWaiter(EXCLUSIVE)同步队列✅超时后标记 CANCELLED条件等待addConditionWaiter()条件队列❌必须持有锁清理 CANCELLED 尾节点条件唤醒单个transferForSignal()同步队列✅CONDITION→0CAS enq条件唤醒全部doSignalAll()同步队列✅遍历转移所有节点中断被动唤醒transferAfterCancelledWait()同步队列✅可能提前唤醒需处理中断模式面试官想要的满分总结AQS 的入队时机是理解其线程调度机制的关键。核心认知有三层同步队列入队4 种变体所有竞争锁失败的路径最终都走addWaiter→enq用 CAS 尾插法保证多线程安全。enq的哨兵 head 设计和先prev后tail的顺序是应对 ABA 和并发断裂的关键。条件队列入队2 种变体await系列方法在持有锁的前提下入队单线程操作无需 CAS但需清理 CANCELLED 尾节点防止链表污染。队列间转移3 种变体signal/signalAll通过 CAS 将节点从条件队列CONDITION状态转移到同步队列0状态转移后不一定立即唤醒而是等待前驱释放。中断导致的被动转移是边界情况需处理THROW_IE/REINTERRUPT两种中断模式。工程实践中避免在tryAcquire中嵌套锁监控同步队列长度确保所有await都有对应的signal。AQS 的入队设计体现了用锁简化单线程路径用 CAS 支持高并发路径的精妙平衡。觉得对您有帮助麻烦点点关注啦您的关注是我创作的最大动力~