AQS双向队列状态迁移源码剖析
AQS双向队列状态迁移源码剖析前言AQS双向队列状态迁移源码剖析一、 AQS 双队列架构概述二、 核心流程从 await() 到 signal() 的节点转移三、 OpenJDK8源码详细逐行剖析1. 节点的产生与入队Condition Queue2. 彻底释放锁AQS State 归零3. 唤醒与节点转移的桥梁signal()4. 转移的核心实现transferForSignal(Node)5. 同步队列的尾插法enq(Node)6. 自旋检测isOnSyncQueue(Node)四、 核心设计思想与工程细节1. **为什么在 transferForSignal 中改变状态要用 CAS**2. **前驱节点状态转化为 Node.SIGNAL 的深意**3. **无锁化Lock-Free的双向队列尾插安全**前言本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限文中内容难免存在疏漏恳请读者不吝指正。AQS双向队列状态迁移源码剖析在Java并发包java.util.concurrent中AbstractQueuedSynchronizerAQS是构建锁和其他同步组件的核心基石。作为软件开发工程师深入理解其双向队列Sync Queue即同步队列/CLH队列与单向条件队列Condition Queue之间的节点转移机制是掌握高并发底层原理的必经之路。下面我们将结合OpenJDK8的具体源码深度剖析当线程调用Condition.await()和Condition.signal()时AQS 内部节点Node是如何在两个队列之间进行流转与状态切换的。一、 AQS 双队列架构概述AQS 内部维护了两种互补的队列结构同步队列Sync Queue / CLH Queue结构双向链表。通过head和tail指针维护。节点关系节点间通过prev和next指针双向链接。作用管理等待获取锁的线程。条件队列Condition Queue结构单向链表。由 AQS 的内部类ConditionObject维护通过firstWaiter和lastWaiter指针管理。节点关系节点间仅通过nextWaiter指针单向链接。作用管理因未满足特定条件而调用await()进入等待状态的线程。这两个队列使用的都是 AQS 的静态内部类Node但节点的关键属性和状态waitStatus在不同队列中承载着不同的含义waitStatus Node.CONDITION (-2)代表节点存在于条件队列中。waitStatus Node.SIGNAL (-1)代表该节点的后继节点需要被唤醒存在于同步队列中。waitStatus 0初始化状态。二、 核心流程从await()到signal()的节点转移当一个持有锁的线程调用condition.await()时它会释放锁并进入条件队列挂起当另一个线程调用condition.signal()时处于条件队列首部的节点将被转移到同步队列中等待重新获取锁。这一过程的底层转移逻辑由以下几个核心步骤组成[持有锁的线程] - 调用 await() - 释放锁 | v (封装为 Node.CONDITION) [Condition Queue (单向)] - 线程挂起 (LockSupport.park) | | (另一个线程调用 signal()) v [transferForSignal] - CAS修改状态为0 - 移出 Condition Queue | v (调用 enq() 尾插) [Sync Queue (双向)] - 前驱节点状态设为 SIGNAL - 等待/唤醒三、 OpenJDK8源码详细逐行剖析1. 节点的产生与入队Condition Queue当线程调用await()时首先会调用addConditionWaiter()将当前线程封装为Node并放入条件队列尾部。// java.util.concurrent.locks.AbstractQueuedSynchronizer.ConditionObjectpublicfinalvoidawait()throwsInterruptedException{if(Thread.interrupted())thrownewInterruptedException();// 1. 将当前线程封装为 Node加入到 Condition 队列尾部NodenodeaddConditionWaiter();// 2. 释放当前线程持有的所有锁AQS state并返回释放前的 state以便后续重入intsavedStatefullyRelease(node);intinterruptMode0;// 3. 自旋判断如果当前节点不在同步队列中则挂起线程while(!isOnSyncQueue(node)){LockSupport.park(this);// 线程在此处阻塞挂起// 如果在挂起期间或被唤醒时发生中断则跳出循环处理中断if((interruptModecheckInterruptWhileWaiting(node))!0)break;}// 4. 此时节点已被转移到同步队列调用 acquireQueued 尝试在同步队列中阻塞获取锁if(acquireQueued(node,savedState)interruptMode!THROW_IE)interruptModeREINTERRUPT;if(node.nextWaiter!null)// 清理条件队列中被取消的节点unlinkCancelledWaiters();if(interruptMode!0)reportInterruptAfterWait(interruptMode);}/** * 将当前线程作为等待者添加到条件队列的尾部 */privateNodeaddConditionWaiter(){NodetlastWaiter;// 如果尾部节点已被取消waitStatus 不是 CONDITION则进行清理if(t!nullt.waitStatus!Node.CONDITION){unlinkCancelledWaiters();tlastWaiter;}// 创建一个 waitStatus 为 CONDITION (-2) 的新节点NodenodenewNode(Thread.currentThread(),Node.CONDITION);if(tnull)firstWaiternode;// 队列为空初始化头部elset.nextWaiternode;// 单向链表尾插lastWaiternode;// 更新尾指针returnnode;}2. 彻底释放锁AQS State 归零进入条件队列后线程必须完全释放所持有的锁否则会造成死锁。// java.util.concurrent.locks.AbstractQueuedSynchronizer/** * 释放当前节点持有的所有锁还原 state */finalintfullyRelease(Nodenode){booleanfailedtrue;try{intsavedStategetState();// 释放独占锁若释放失败如未持有锁调用 await会抛出 IllegalMonitorStateExceptionif(release(savedState)){failedfalse;returnsavedState;}else{thrownewIllegalMonitorStateException();}}finally{if(failed)// 释放失败则将节点状态置为 CANCELLED 废弃node.waitStatusNode.CANCELLED;}}3. 唤醒与节点转移的桥梁signal()当另一个线程调用signal()时AQS 开始将条件队列头部的有效节点转移至同步队列中。// java.util.concurrent.locks.AbstractQueuedSynchronizer.ConditionObjectpublicfinalvoidsignal(){// 1. 验证当前线程是否持有独占锁if(!isHeldExclusively())thrownewIllegalMonitorStateException();NodefirstfirstWaiter;if(first!null)// 2. 唤醒条件队列中的第一个节点doSignal(first);}/** * 循环遍历条件队列找到第一个未取消的节点并将其转移到同步队列中 */privatevoiddoSignal(Nodefirst){do{// 将 firstWaiter 指向下一个节点断开当前节点的单向链接if((firstWaiterfirst.nextWaiter)null)lastWaiternull;first.nextWaiternull;// 帮助 GC}while(!transferForSignal(first)// 核心转移方法如果转移失败说明该节点已取消则继续尝试下一个(firstfirstWaiter)!null);}4. 转移的核心实现transferForSignal(Node)transferForSignal是整个 AQS 双向队列与条件队列交互最精妙的地方。它利用无锁 CAS 操作安全地改变节点状态并将节点拼接到同步队列尾部。// java.util.concurrent.locks.AbstractQueuedSynchronizer/** * 将节点从条件队列转移到同步队列。若成功则返回 true。 */finalbooleantransferForSignal(Nodenode){/* * 1. 尝试将节点状态由 CONDITION (-2) 通过 CAS 修改为初始状态 0。 * 如果修改失败说明该节点在被唤醒前由于超时或中断已经被取消CANCELLED。 */if(!compareAndSetWaitStatus(node,Node.CONDITION,0))returnfalse;/* * 2. 调用 enq 将该节点尾插到同步队列Sync Queue中。 * 注意enq 方法是死循环自旋的 CAS 插入并返回该节点在同步队列中的【前驱节点 p】。 */Nodepenq(node);intwsp.waitStatus;/* * 3. 核心优化 * 如果前驱节点 p 已经处于取消状态 (ws 0) * 或者尝试将前驱节点 p 的状态修改为 SIGNAL (-1) 失败说明前驱节点在此期间状态发生了改变如取消 * 那么为了安全起见我们必须立刻调用 LockSupport.unpark 唤醒当前节点的线程。 * 被唤醒的线程会在 await() 中的 acquireQueued() 流程中重新同步、自适应修正状态。 * * 如果 CAS 设置前驱为 SIGNAL 成功则当前线程可以继续保持 park 状态 * 因为前驱节点在释放锁时release 流程会负责将当前线程唤醒。 */if(ws0||!compareAndSetWaitStatus(p,ws,Node.SIGNAL))LockSupport.unpark(node.thread);returntrue;}5. 同步队列的尾插法enq(Node)enq方法负责在多线程并发竞争下安全地将节点插入到双向同步队列的尾部并建立起双向链接关系。// java.util.concurrent.locks.AbstractQueuedSynchronizer/** * 将节点插入同步队列尾部必要时进行初始化。 * return 返回插入节点的前驱节点 */privateNodeenq(finalNodenode){for(;;){// 死循环自旋确保 CAS 成功Nodettail;if(tnull){// 1. 队列为空进行初始化if(compareAndSetHead(newNode()))// CAS 创建哑节点 (Dummy Node) 作为 headtailhead;// 此时 head 和 tail 指向同一个哑节点}else{// 2. 队列不为空将新节点的前驱指针 prev 指向当前的 tailnode.prevt;// 3. CAS 尝试将 tail 指向新节点 nodeif(compareAndSetTail(t,node)){// 4. 关键点CAS 成功后将原尾节点的 next 指针指向新节点完成双向链表的闭环t.nextnode;returnt;// 返回前驱节点}}}}6. 自旋检测isOnSyncQueue(Node)在await()的挂起循环中线程通过isOnSyncQueue判断自己是否已经被成功转移到同步队列中。只有当节点存在于同步队列时它才有资格去竞争锁。// java.util.concurrent.locks.AbstractQueuedSynchronizer/** * 判断节点是否已经在同步队列Sync Queue中 */finalbooleanisOnSyncQueue(Nodenode){// 1. 如果状态还是 CONDITION或者前驱指针 prev 仍为 null则一定还在条件队列中if(node.waitStatusNode.CONDITION||node.prevnull)returnfalse;// 2. 如果 next 指针不为空说明它一定在同步队列中因为条件队列只用 nextWaiterif(node.next!null)returntrue;/* * 3. 如果 node.prev ! null 但 node.next null说明该节点可能正在执行 enq 入队。 * 在 enq() 中是先执行 node.prev t再执行 CAS tail最后才执行 t.next node。 * 此时需要从尾部向前遍历同步队列做最后的确认。 */returnfindNodeFromTail(node);}/** * 从同步队列尾部向前遍历寻找是否存在该节点 */privatebooleanfindNodeFromTail(Nodenode){Nodettail;for(;;){if(tnode)returntrue;if(tnull)returnfalse;tt.prev;}}四、 核心设计思想与工程细节作为软件开发工程师我们可以从这段优雅的代码中总结出 AQS 极具工业级水准的底层设计细节1.为什么在transferForSignal中改变状态要用 CAS在多线程环境下一个处于条件队列中挂起的线程可能在被signal()唤醒的同时遭遇了线程中断或超时。如果发生了中断线程自身会调用transferAfterCancelledWait尝试将自己移入同步队列。这就会与signal()线程产生并发冲突。AQS 通过compareAndSetWaitStatus(node, Node.CONDITION, 0)这一行 CAS 操作精妙地充当了“仲裁者”。谁 CAS 成功谁就负责将该节点放入同步队列。失败的一方则避开重复入队从而保证了线程状态的一致性。2.前驱节点状态转化为Node.SIGNAL的深意在同步队列中AQS 采用了一种被动唤醒机制一个节点在同步队列中被挂起后它不需要不断自旋而是靠它的前驱节点在释放锁时来唤醒它。因此在transferForSignal成功将节点尾插到同步队列后必须将前驱节点的状态改为Node.SIGNAL (-1)。这相当于新节点对前驱节点说“老哥我排在你后面了你待会儿释放锁的时候记得叫醒我。”如果前驱节点已经取消或者修改状态失败当前线程就会立刻被unpark唤醒防止其在同步队列中永久“失联”。3.无锁化Lock-Free的双向队列尾插安全在enq(Node node)中双向链表的构建分为三步node.prev t;线程本地安全操作CAS 改变tail指针原子分水岭t.next node;多线程可见这种顺序导致了在极短的瞬间第 2 步完成第 3 步未完成时从 head 顺着next指针往后找可能找不到新节点但从 tail 逆着prev指针往前找是一定能找到的。这也是为什么isOnSyncQueue的退化检查方法findNodeFromTail必须从后往前from tail遍历的原因。

相关新闻

AI-Based Measurement of Innovation: Mapping Expert Insight into Large Language Model Applications

AI-Based Measurement of Innovation: Mapping Expert Insight into Large Language Model Applications

文章主要内容总结 本文提出了一种名为MicroMix的混合精度量化框架,专门针对大型语言模型(LLMs)在NVIDIA Blackwell架构上的高效推理设计。该框架结合了基于微缩放(Microscaling, MX)数据格式的量化算法和矩阵乘法内核,通过灵活分配MXFP4、MXFP6、MXFP8精度通道,在保证模…

2026/10/27 1:46:03 阅读更多 →
如何快速压缩游戏ISO文件:tochd让你的游戏收藏节省50%硬盘空间

如何快速压缩游戏ISO文件:tochd让你的游戏收藏节省50%硬盘空间

如何快速压缩游戏ISO文件:tochd让你的游戏收藏节省50%硬盘空间 【免费下载链接】tochd Convert game ISO and archives to CD/DVD CHD for emulation on Linux. 项目地址: https://gitcode.com/gh_mirrors/to/tochd 还在为海量游戏ISO文件占用宝贵硬盘空间而…

2026/10/23 21:04:42 阅读更多 →
LSTM自编码器在肠道微生物时序异常检测中的应用

LSTM自编码器在肠道微生物时序异常检测中的应用

1. 项目概述:肠道微生物时序异常检测实战 在血液肿瘤治疗领域,造血干细胞移植(HCT)后患者的肠道菌群动态监测是临床难题。传统微生物组分析往往局限于单时间点采样,而忽略了菌群演化的时序特性——这正是我们开发DynaB…

2026/10/25 12:35:25 阅读更多 →

最新新闻

Electron桌面应用开发实战:从环境搭建到打包发布全流程

Electron桌面应用开发实战:从环境搭建到打包发布全流程

做前端这些年,我大部分时间都在跟浏览器标签页打交道,直到第一次把桌面应用跑起来,才发现原来自己一直被限制在地址栏和刷新按钮围起来的框框里。Electron这个词,最开始是在某段时间频繁出现在同事的讨论里,一个用HTML…

2026/10/28 23:48:14 阅读更多 →
滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

简介:亚太天能T109滑盖智能锁产品说明书是一份PDF格式的官方技术文档,主要面向智能锁安装人员、终端用户及售后维护者,帮助快速掌握产品安装、配置与常见故障处理。全包仅1个PDF文件,大小4.37MB,内容结构清晰&#xff…

2026/10/29 8:45:01 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

上个月我在调试一台高速AGV的驱动系统,整车要求空载跑到8m/s,电机转速已经拉到额定点,驱动器的输出电压也顶到了母线电压极限附近。再往上踩速度指令,转速纹丝不动,三相电流反而开始震荡。那会儿我的第一反应就是&…

2026/10/28 9:02:11 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

C++核心知识点:从内存管理到STL的实战指南

C 这门语言我前前后后折腾了十几年,用它写过嵌入式驱动、做过游戏服务端,也搭过基础组件库。每次看到新人一头扎进"C 核心知识点"这个搜索词,我其实挺感慨的——这门语言的难点从来不是语法多,而是知识点之间的耦合太深…

2026/10/28 22:10:30 阅读更多 →
T109滑盖智能锁怎么装?安装调试与故障排查全攻略

T109滑盖智能锁怎么装?安装调试与故障排查全攻略

简介:亚太天能T109滑盖智能锁产品说明书以PDF格式发布,适合智能锁用户、安装人员与售后维修人员阅读。内容围绕T109系列指纹密码刷卡锁展开,先介绍前面板、后面板、锁体、机械钥匙、电池盒盖等部件名称与功能,再给出完整安装步骤&…

2026/10/28 9:00:54 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

LLM知识降熵:从信息垄断到理解权下放

你有没有过这种经历:翻开源码注释、论文摘要或者一份专业报告,每个字都认识,连在一起却像天书。想找人聊聊,身边要么没人懂,要么懂的懒得跟你解释。后来我试着把同一段内容丢给一个大语言模型,几秒钟之后&a…

2026/10/29 8:44:28 阅读更多 →

日新闻

周新闻

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/28 16:58:40 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/28 9:01:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/28 19:25:09 阅读更多 →