生产环境死锁排查:从线程堆栈到锁等待图的完整分析链路
生产环境死锁排查从线程堆栈到锁等待图的完整分析链路一、所有请求线程突然卡住不动日志一天没打一行生产环境最诡异的故障之一服务还活着端口还开着健康检查还能过但所有请求都超时。查 CPU 使用率——几乎为零。查内存——正常范围。查数据库连接池——有很多空闲。但就是不响应任何请求。这种症状在 Java 应用中十有八九是发生了死锁。死锁的根源很简单两个或多个线程相互等待对方释放锁资源形成闭环等待谁也走不出去。但排查死锁的过程不是谁都能靠直觉完成的——必须从现象出发沿着数据收集、堆栈分析、锁等待图构建的链路一步步走最终定位到是哪两个线程在哪两把锁上互相卡住。排查死锁的完整链路如下发现异常请求超时、CPU空闲后先收集第一手数据使用jstack导出线程堆栈同时查看GC日志排除GC停顿检查数据库连接排除DB层死锁。拿到线程堆栈后定位所有处于BLOCKED状态的线程提取每个线程等待的锁地址构建锁等待有向图。检查图中是否存在环若存在环即可确认死锁定位环中的线程和锁若不存在环则属于非死锁问题需排查其他原因。确认死锁后分析死锁的业务逻辑通过排序锁获取顺序等方式修复最后验证修复并上线。二、从 jstack 中读取死锁信息在 Java 中排查死锁的第一步是jstack pid。这条命令会打印所有线程的当前堆栈并自动检测死锁。如果能直接看到 JVM 打印的Found one Java-level deadlock信息恭喜你死锁已经被 JVM 自动检测出来了。但有些不那么经典的死锁JVM 可能检测不到——比如通过CountDownLatch、Semaphore或线程池造成的逻辑死锁这些不涉及 JVM 锁监视器jstack 不会主动报告。这种情况下需要手动分析线程堆栈。以下是两个线程互相等待的典型堆栈片段Thread-A #23 prio5 os_prio0 tid0x00007f8a0c001000 nid0x5a3b waiting for monitor entry [0x00007f89e8bf9000] ---java.lang.Thread.State: BLOCKED (on object monitor)at com.example.OrderService.createOrder(OrderService.java:45)- waiting to lock 0x00000006c2e8d3f0 (a com.example.LockA)- locked 0x00000006c2e8d400 (a com.example.LockB)Thread-B #24 prio5 os_prio0 tid0x00007f8a0c001800 nid0x5a3c waiting for monitor entry [0x00007f89e8af8000]java.lang.Thread.State: BLOCKED (on object monitor)at com.example.OrderService.refundOrder(OrderService.java:78)- waiting to lock 0x00000006c2e8d400 (a com.example.LockB)- locked 0x00000006c2e8d3f0 (a com.example.LockA)从这两段堆栈可以清晰地看到Thread-A 持有 LockB等待 LockAThread-B 持有 LockA等待 LockB。典型的环状等待——死锁。 读取 jstack 时需要关注的关键信息 - **线程状态**BLOCKED 表示在等待 JVM 锁WAITING 表示在等待信号。 - **waiting to lock**线程正在等待的锁对象地址。 - **locked**线程已经持有的锁对象地址。 - **对象地址的唯一性**同一个地址 0x00000006c2e8d3f0 出现在两个线程的 waiting to lock 和 locked 中就构成了等待环。 ## 三、构建锁等待图 当线程数量超过 10 个时肉眼分析堆栈信息效率太低。一个更系统的方法是构建锁等待图Lock Wait Graph。 java /** * 锁等待图分析工具 * * 通过 jstack 输出解析线程和锁的等待关系 * 构建有向图并检测环死锁 * * 图的节点是锁对象用地址标识 * 图的边是A → B 表示持有锁 A 的线程在等待锁 B */ public class LockWaitGraphAnalyzer { /** * 从 jstack 输出中检测死锁 * * param jstackOutput jstack 命令的完整文本输出 * return 检测到的死锁环列表 */ public ListListString detectDeadlocks(String jstackOutput) { // 构建锁等待图 MapString, SetString waitGraph buildWaitGraph(jstackOutput); // 在有向图中使用 DFS 检测环 return findCycles(waitGraph); } /** * 从 jstack 文本中构建锁等待关系 */ private MapString, SetString buildWaitGraph(String jstackOutput) { MapString, SetString graph new HashMap(); // 按线程分段 String[] threadBlocks jstackOutput.split(\n\n); for (String block : threadBlocks) { if (!block.contains(BLOCKED)) { continue; // 只关注等待锁的线程 } // 解析 locked该线程持有的锁 SetString heldLocks new HashSet(); Pattern lockedPattern Pattern.compile( - locked (0x[0-9a-f])); Matcher lockedMatcher lockedPattern.matcher(block); while (lockedMatcher.find()) { heldLocks.add(lockedMatcher.group(1)); } // 解析 waiting to lock该线程等待的锁 Pattern waitingPattern Pattern.compile( - waiting to lock (0x[0-9a-f])); Matcher waitingMatcher waitingPattern.matcher(block); while (waitingMatcher.find()) { String waitingLock waitingMatcher.group(1); // 对每个持有的锁添加一条持有 → 等待的边 for (String heldLock : heldLocks) { graph.computeIfAbsent(heldLock, k - new HashSet()) .add(waitingLock); } } } return graph; } /** * DFS 检测有向图中的环 * * 维护三种状态 * - 白色未访问 * - 灰色正在访问在当前递归路径上 * - 黑色已访问完毕 * * 如果在 DFS 中遇到灰色节点说明存在环死锁 */ private ListListString findCycles(MapString, SetString graph) { ListListString cycles new ArrayList(); SetString whiteSet new HashSet(graph.keySet()); SetString graySet new HashSet(); SetString blackSet new HashSet(); while (!whiteSet.isEmpty()) { String start whiteSet.iterator().next(); ListString currentPath new ArrayList(); dfs(start, graph, whiteSet, graySet, blackSet, currentPath, cycles); } return cycles; } private boolean dfs(String current, MapString, SetString graph, SetString white, SetString gray, SetString black, ListString path, ListListString cycles) { white.remove(current); gray.add(current); path.add(current); for (String neighbor : graph.getOrDefault(current, Collections.emptySet())) { if (black.contains(neighbor)) { continue; // 已探索完成无需再走 } if (gray.contains(neighbor)) { // 遇到灰色节点发现环 // 从当前路径中提取环的部分 int cycleStart path.indexOf(neighbor); ListString cycle new ArrayList( path.subList(cycleStart, path.size())); cycles.add(cycle); return true; } if (dfs(neighbor, graph, white, gray, black, path, cycles)) { return true; } } path.remove(path.size() - 1); gray.remove(current); black.add(current); return false; } }四、死锁修复的三种方案定位到死锁后修复的方向有三个选择哪一种取决于业务场景。方案一统一加锁顺序。这是死锁问题的根本解决方案。如果多个业务方法都需要获取 LockA 和 LockB确保在所有方法中加锁的顺序一致总是先 LockA 再 LockB。这样不会形成环状等待。缺点是需要对代码有一定的控制力如果锁的获取路径分散在多个微服务中统一顺序就很难保证。方案二使用 tryLock 加超时。不用synchronized改用ReentrantLock.tryLock(timeout, TimeUnit)。获取不到锁时主动放弃释放已持有的锁过一段时间再重试整个业务逻辑。这种方式避免了永久死锁但引入了新的问题业务可能在多次重试后才成功增加了整体延迟。/** * 使用 tryLock 避免死锁 * * 核心思路 * - 获取第一把锁后用 tryLock 获取第二把锁 * - 获取失败时释放第一把锁随机等待后重试 * - 避免了永久等待把死锁转化为偶尔的重试 */ public boolean safeTransfer(Lock from, Lock to, int amount) { while (true) { from.lock(); try { // 用 tryLock 而不是 lock避免永久等待 if (to.tryLock(100, TimeUnit.MILLISECONDS)) { try { // 两把锁都拿到了执行业务逻辑 doTransfer(from, to, amount); return true; } finally { to.unlock(); } } } finally { from.unlock(); } // 没拿到第二把锁短暂随机休眠后重试 // 加随机休眠是为了避免多个线程同步重试 Thread.sleep(ThreadLocalRandom.current().nextLong(50, 200)); } }方案三缩小锁的粒度。不是对整个方法加锁而是只对需要互斥访问的数据加锁。比如扣减库存时锁定的是特定的商品 ID 范围而不是整个库存服务。锁的粒度越细不同线程锁冲突的概率越低死锁的可能性也越小。五、总结死锁排查的核心是数据驱动——从 jstack 导出堆栈提取线程等待关系构建锁等待图检测环结构定位到具体的代码行。这个过程有固定的模式不需要太多直觉。修复方案中统一加锁顺序是根本解tryLock 加超时是工程解缩小锁粒度是架构解。三者可以根据场景组合使用。最重要的是不要在代码评审时觉得这种加锁方式一般不会出问题——只要存在环状等待的可能性死锁就一定会发生只是时间早晚的问题。

相关新闻

ES6-learning:从零开始学习ES6的简单教程

ES6-learning:从零开始学习ES6的简单教程

ES6-learning:从零开始学习ES6的简单教程 【免费下载链接】ES6-learning 《深入理解ES6》教程学习笔记 项目地址: https://gitcode.com/gh_mirrors/es6l/ES6-learning ES6-learning项目是一个完整、系统的ES6学习教程,基于《深入理解ES6》书籍的学…

2026/10/10 4:18:12 阅读更多 →
Lyric-Getter API使用指南:轻松集成歌词功能到你的应用

Lyric-Getter API使用指南:轻松集成歌词功能到你的应用

Lyric-Getter API使用指南:轻松集成歌词功能到你的应用 【免费下载链接】Lyric-Getter Lyric Getter | 酒域-歌词获取 项目地址: https://gitcode.com/gh_mirrors/ly/Lyric-Getter Lyric-Getter是一款强大的歌词获取工具,通过其提供的API接口&…

2026/10/9 3:20:58 阅读更多 →
GKNavigationBarViewController与系统导航栏对比:为什么选择自定义方案?

GKNavigationBarViewController与系统导航栏对比:为什么选择自定义方案?

GKNavigationBarViewController与系统导航栏对比:为什么选择自定义方案? 【免费下载链接】GKNavigationBarViewController iOS自定义导航栏-导航栏联动 项目地址: https://gitcode.com/gh_mirrors/gk/GKNavigationBarViewController 在iOS应用开发…

2026/10/11 18:32:15 阅读更多 →

最新新闻

基于滑动窗口去重设计轻量实时事件聚合器:从需求到落地

基于滑动窗口去重设计轻量实时事件聚合器:从需求到落地

之前我负责的一个内部工具项目,代号就叫 rea,全称后来定为 Realtime Event Aggregator。最初催生这个项目的需求并不复杂:多条业务线不停地产生事件,每秒钟少则几千、多则几万条,我们要把这些事件统一收上来&#xff0…

2026/10/11 22:21:04 阅读更多 →
Python局部敏感哈希实战:论文查重与相似度比对

Python局部敏感哈希实战:论文查重与相似度比对

简介:这是一份面向计算机专业学生与毕业设计开发者的Python实战资源,围绕局部敏感哈希(LSH)算法实现论文相似性比对。资源从LSH的(r1,r2,p1,p2)敏感性定义出发,讲解相似对象映射为同一哈希值概率高、不相似对象概率低的…

2026/10/11 22:21:04 阅读更多 →
日化广告投放数据分析报告:从数据归集到PPT模板的完整流程

日化广告投放数据分析报告:从数据归集到PPT模板的完整流程

简介:这份日化广告投放数据分析报告PPT模板面向日化行业营销人员、广告投放策略制定者及希望提升数据分析能力的市场从业者,帮助其快速搭建专业的数据汇报框架,理清广告效果评估与策略优化思路。压缩包共3个文件,以一份可直接编辑…

2026/10/11 22:21:04 阅读更多 →
evlog CLI 可观测性评分实战:用 evlog map 揪出应用里的暗区日志入口

evlog CLI 可观测性评分实战:用 evlog map 揪出应用里的暗区日志入口

【免费下载链接】evlog Digging through logs is not observability. Its hope — wide events, structured errors, TypeScript-first, every runtime. 项目地址: https://gitcode.com/gh_mirrors/ev/evlog 点击查看 免费下载 为什么需要 evlog 可观测性评分 翻日…

2026/10/11 22:21:04 阅读更多 →
agent-skills:面向生产环境的智能体能力治理范式

agent-skills:面向生产环境的智能体能力治理范式

1. 项目概述:一个被严重低估的“技能容器”概念“agent-skills”这个词组乍看像技术黑话,但拆开来看——它既不是某个具体开源库的官方命名,也不是某家大厂刚发布的API产品名,而是一个正在快速凝聚共识的设计范式代号。我在过去三…

2026/10/11 22:21:04 阅读更多 →
pip安装报错No space left on device?磁盘与inode排查详解

pip安装报错No space left on device?磁盘与inode排查详解

今天帮一位同事排查安装报错时,又看到了那行红色大字:ERROR: Could not install packages due to an OSError: [Errno 28] No space left on device。这行提示我见过太多次,几乎每个用 Python 做开发的人迟早都会撞上,尤其是服务器…

2026/10/11 22:20:04 阅读更多 →

日新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/11 0:00:27 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/11 0:00:27 阅读更多 →

月新闻

我发现了一个新思路:用 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/11 10:45:37 阅读更多 →
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/11 14:36:53 阅读更多 →
黑夜航拍船只数据集训练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/11 14:36:54 阅读更多 →