news 2026/6/13 21:19:54

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【大白话说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){Nodenode=newNode(Thread.currentThread(),mode);Nodepred=tail;// 快速路径:tail 已初始化,直接 CAS 入队if(pred!=null){node.prev=pred;if(compareAndSetTail(pred,node)){pred.next=node;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/tryAcquireNanos

    publicfinalvoidacquireInterruptibly(intarg)throwsInterruptedException{if(Thread.interrupted())thrownewInterruptedException();if(!tryAcquire(arg))doAcquireInterruptibly(arg);// 入队 + 可中断阻塞}

    与普通acquire的区别:阻塞期间响应中断,直接抛出InterruptedException

  • 1.4enq方法——自旋初始化 + 尾插法

    privateNodeenq(finalNodenode){for(;;){Nodet=tail;if(t==null){// 队列未初始化if(compareAndSetHead(newNode()))// CAS 设置哨兵 headtail=head;}else{node.prev=t;if(compareAndSetTail(t,node)){// CAS 尾插t.next=node;returnt;}}}}

    关键设计

    • 哨兵 head:初始 head 是一个空 Node(thread=null),不绑定任何线程,只作为队列起点;
    • 自旋保证原子性compareAndSetTail失败则重试,直到成功;
    • ABA 安全:虽然tail可能 ABA(被其他线程修改又改回),但prev指针保证了链表的完整性。
2. 条件队列(Condition Queue)的入队时机
  • 2.1 时机四:调用Condition.await()
    当线程持有锁并调用await()时,会释放锁并加入条件队列:

    publicfinalvoidawait()throwsInterruptedException{if(Thread.interrupted())thrownewInterruptedException();Nodenode=addConditionWaiter();// 加入条件队列intsavedState=fullyRelease(node);// 完全释放锁// ... 阻塞等待}

    addConditionWaiter的入队逻辑:

    privateNodeaddConditionWaiter(){Nodet=lastWaiter;// 清理已取消的尾节点if(t!=null&&t.waitStatus!=Node.CONDITION){unlinkCancelledWaiters();t=lastWaiter;}Nodenode=newNode(Thread.currentThread(),Node.CONDITION);if(t==null)firstWaiter=node;elset.nextWaiter=node;lastWaiter=node;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();Nodefirst=firstWaiter;if(first!=null)doSignal(first);}

    transferForSignal的转移逻辑:

    finalbooleantransferForSignal(Nodenode){// 步骤1:将 CONDITION 状态改为 0(CAS 保证原子性)if(!compareAndSetWaitStatus(node,Node.CONDITION,0))returnfalse;// 节点已取消// 步骤2:入队到同步队列尾部Nodep=enq(node);intws=p.waitStatus;// 步骤3:如果前驱已取消或设置 SIGNAL 失败,直接唤醒if(ws>0||!compareAndSetWaitStatus(p,ws,Node.SIGNAL))LockSupport.unpark(node.thread);returntrue;}

    关键细节

    • CAS 状态变更CONDITION( -2) → 0是原子操作,确保节点在转移过程中不会被其他线程操作;
    • 转移后不一定立即唤醒:如果前驱正常且设置 SIGNAL 成功,节点等待前驱释放时唤醒;只有前驱异常时才立即unpark
  • 3.2 时机七:调用Condition.signalAll()
    doSignalAll遍历整个条件队列,将所有节点逐个转移到同步队列:

    privatevoiddoSignalAll(Nodefirst){lastWaiter=firstWaiter=null;do{Nodenext=first.nextWaiter;first.nextWaiter=null;transferForSignal(first);first=next;}while(first!=null);}
  • 3.3 时机八:中断导致的被动转移
    线程在条件队列中等待时被中断,会触发transferAfterCancelledWait

    privateintcheckInterruptWhileWaiting(Nodenode){returnThread.interrupted()?(transferAfterCancelledWait(node)?THROW_IE:REINTERRUPT):0;}

    中断后节点会被强制转移到同步队列,但此时可能尚未被signal,属于"提前唤醒"。

4. 特殊入队时机——CANCELLED 节点的清理
  • 4.1 时机九:获取锁超时/中断后标记为 CANCELLED
    当线程在acquireQueued中因中断或超时而取消时:

    privatevoidcancelAcquire(Nodenode){if(node==null)return;node.thread=null;// 跳过前驱的 CANCELLED 节点Nodepred=node.prev;while(pred.waitStatus>0)node.prev=pred=pred.prev;NodepredNext=pred.next;node.waitStatus=Node.CANCELLED;// 如果当前节点是 tail,直接移除if(node==tail&&compareAndSetTail(node,pred)){compareAndSetNext(pred,predNext,null);}else{// 否则,让前驱负责清理(在 shouldParkAfterFailedAcquire 中)if(pred!=head&&pred.waitStatus==Node.SIGNAL||compareAndSetWaitStatus(pred,0,Node.SIGNAL)&&pred.thread!=null){Nodenext=node.next;if(next!=null&&next.waitStatus<=0)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()且持有锁compareAndSetWaitStatus+enqCONDITION → 等待锁
signalAll()同步队列调用signalAll()多次transferForSignalCONDITION → 等待锁
中断被动转移同步队列条件队列中线程被中断transferAfterCancelledWaitCONDITION → 等待锁
超时取消同步队列(CANCELLED)tryAcquireNanos超时cancelAcquire阻塞 → CANCELLED
6. 入队过程中的并发安全问题
  • 6.1tail的 ABA 问题

    时间线: T1: 线程 A 读取 tail = NodeX T2: 线程 B CAS tail = NodeY T3: 线程 C CAS tail = NodeX(NodeX 被重新入队) T4: 线程 A CAS tail = NodeZ(基于旧的 NodeX,但 NodeX 已非原节点)

    解决方案node.prev = pred先设置,即使tailABA,也能通过prev指针构建完整链表。

  • 6.2next指针的可见性
    next指针是普通变量(非 volatile),依赖tail的 happens-before 保证可见性:

    // enq 中的顺序:node.prev=t;// 普通写compareAndSetTail(t,node);// volatile 写( happens-before 屏障)t.next=node;// 普通写,对后续读 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 监控队列长度

    ReentrantLocklock=newReentrantLock();// 入队后检查队列长度if(lock.getQueueLength()>100){logger.warn("AQS queue too long: {}",lock.getQueueLength());}
8. 面试官追问与高分回答模板
  • 追问 1:“AQS 中节点的入队时机有哪些?”

    • 低分回答:“竞争失败入同步队列,await 入条件队列,signal 转移队列。”(遗漏了多种变体)
    • 高分回答

      "AQS 的入队时机可分为三大类九种情况:

      1. 同步队列入队(4 种):独占锁acquire失败、共享锁acquireShared失败、可中断acquireInterruptibly失败、限时tryAcquireNanos失败;
      2. 条件队列入队(2 种):await()awaitUninterruptibly/awaitNanos/awaitUntil
      3. 队列间转移(3 种):signal()转移头节点、signalAll()转移全部节点、中断导致的被动转移。

      此外,超时/中断会导致节点被标记为 CANCELLED,这也是一种特殊的"状态变更"。

      关键区别:同步队列入队需要 CAS(多线程竞争),条件队列入队无需 CAS(单线程持有锁),转移时需要 CAS 变更waitStatus并调用enq。"

  • 追问 2:“同步队列的入队为什么需要 CAS?条件队列为什么不需要?”

    • 高分回答

      "同步队列管理的是竞争锁失败的线程,这些线程来自多个 CPU 核心,同时尝试入队,必须用 CAS 保证尾插法的原子性(compareAndSetTail)。

      条件队列管理的是调用 await() 的线程,而await()的调用前提是当前线程必须持有锁。既然持有锁,同一时刻只有一个线程能操作条件队列,因此是单线程操作,无需 CAS。

      这是 AQS 设计的精妙之处:用锁保护条件队列(简化实现),用 CAS 保护同步队列(支持高并发)。"

  • 追问 3:“enq方法为什么要用哨兵 head?直接让第一个线程作为 head 不行吗?”

    • 高分回答

      "哨兵 head(thread=null的空节点)有两个核心作用:

      1. 简化边界处理acquireQueued中判断p == head时尝试获取锁,如果 head 绑定真实线程,需要额外处理线程已释放但节点未出队的情况;
      2. 统一释放逻辑release时唤醒head.next,如果 head 是真实线程且已退出,需要特殊处理。哨兵 head 保证队列始终有头节点,释放逻辑统一。

      另外,哨兵 head 的waitStatus可以承载 SIGNAL 状态,提示后继节点"我释放时会唤醒你"。"

  • 追问 4:“signal后节点一定立即被唤醒吗?”

    • 高分回答

      "不一定。transferForSignal将节点从条件队列转移到同步队列后,有两种情况:

      1. 正常路径:前驱节点waitStatus正常,CAS 设置为 SIGNAL。此时节点进入同步队列等待,直到前驱释放锁时唤醒它;
      2. 快速路径:前驱已取消(ws > 0)或设置 SIGNAL 失败,直接LockSupport.unpark(node.thread)唤醒。

      大部分情况下走正常路径,因为signal调用时通常前驱正常。但极端并发下可能走快速路径。"

  • 追问 5:“CANCELLED 节点为什么不立即从链表中移除?”

    • 高分回答

      "CANCELLED 节点采用延迟清理策略,原因有三:

      1. 避免竞争cancelAcquire时可能持有锁的线程正在遍历链表(如unparkSuccessor),立即移除需要复杂同步;
      2. 简化实现:依赖后继线程的shouldParkAfterFailedAcquire跳过 CANCELLED 节点,将清理责任分散到多个线程;
      3. 尾节点快速移除:如果 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 种变体):所有竞争锁失败的路径最终都走addWaiterenq,用 CAS 尾插法保证多线程安全。enq的哨兵 head 设计和先prevtail的顺序,是应对 ABA 和并发断裂的关键。

条件队列入队(2 种变体):await系列方法在持有锁的前提下入队,单线程操作无需 CAS,但需清理 CANCELLED 尾节点防止链表污染。

队列间转移(3 种变体):signal/signalAll通过 CAS 将节点从条件队列(CONDITION状态)转移到同步队列(0状态),转移后不一定立即唤醒,而是等待前驱释放。中断导致的被动转移是边界情况,需处理THROW_IE/REINTERRUPT两种中断模式。

工程实践中,避免在tryAcquire中嵌套锁,监控同步队列长度,确保所有await都有对应的signal。AQS 的入队设计体现了"用锁简化单线程路径,用 CAS 支持高并发路径"的精妙平衡。


觉得对您有帮助,麻烦点点关注啦,您的关注是我创作的最大动力~ 🎯

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/13 21:16:57

亲测12款论文降AIGC软件,效果最好的竟然是它!

最近真的太多人来问我&#xff1a;"论文 AI 率太高怎么办&#xff1f;学校要求查 AI 检测&#xff0c;连人工改的都不过&#xff01;" 我懂这种焦虑&#xff0c;因为我自己前阵子也踩过坑。各种号称能降低 AI 率的网站试了一圈&#xff0c;有的乱扣格式&#xff0c;有…

作者头像 李华
网站建设 2026/6/13 21:13:52

终极指南:5步免费获取Grammarly Premium高级版的完整解决方案

终极指南&#xff1a;5步免费获取Grammarly Premium高级版的完整解决方案 【免费下载链接】autosearch-grammarly-premium-cookie 免费白嫖使用Grammarly Premium高级版 项目地址: https://gitcode.com/gh_mirrors/au/autosearch-grammarly-premium-cookie 在数字写作领…

作者头像 李华
网站建设 2026/6/13 21:12:56

多组学因子分析完全指南:用MOFA2轻松整合生物大数据

多组学因子分析完全指南&#xff1a;用MOFA2轻松整合生物大数据 【免费下载链接】MOFA2 Multi-Omics Factor Analysis 项目地址: https://gitcode.com/gh_mirrors/mo/MOFA2 在当今生物医学研究领域&#xff0c;多组学数据整合分析已成为揭示复杂疾病机制和生命规律的关键…

作者头像 李华
网站建设 2026/6/13 21:04:38

告别手忙脚乱!用KiCad 7.0高效布线的10个核心快捷键与技巧

告别手忙脚乱&#xff01;用KiCad 7.0高效布线的10个核心快捷键与技巧作为一名长期与PCB设计打交道的工程师&#xff0c;我深知布线环节往往是整个项目中最耗时的部分。KiCad 7.0作为开源EDA工具的代表&#xff0c;其强大的功能和灵活的快捷键系统可以显著提升工作效率。本文将…

作者头像 李华