生产环境死锁排查:从线程堆栈到锁等待图的完整分析链路
生产环境死锁排查从线程堆栈到锁等待图的完整分析链路一、所有请求线程突然卡住不动日志一天没打一行生产环境最诡异的故障之一服务还活着端口还开着健康检查还能过但所有请求都超时。查 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/7/25 13:34:39 阅读更多 →
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/7/25 8:05:17 阅读更多 →
GKNavigationBarViewController与系统导航栏对比:为什么选择自定义方案?

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

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

2026/7/25 13:34:33 阅读更多 →

最新新闻

Miden VM高级特性:自定义Host与Precompiles如何扩展零知识应用边界

Miden VM高级特性:自定义Host与Precompiles如何扩展零知识应用边界

Miden VM高级特性:自定义Host与Precompiles如何扩展零知识应用边界 【免费下载链接】miden-vm STARK-based virtual machine 项目地址: https://gitcode.com/gh_mirrors/mi/miden Miden VM作为基于STARK的虚拟机,通过自定义Host和Precompiles两大…

2026/7/25 23:02:05 阅读更多 →
基于计算机视觉的民宿图片真实性检测系统设计

基于计算机视觉的民宿图片真实性检测系统设计

1. 项目背景与行业痛点去年帮朋友订民宿时遇到件糟心事——平台展示的精修照片和实际房间差距大到离谱。这并非个例,根据行业调研数据显示,超过37%的住客投诉与房源图片失真直接相关。传统人工审核面对海量房源时存在三大死穴:修图技术迭代速…

2026/7/25 23:02:05 阅读更多 →
Windows 11终极优化指南:使用Win11Debloat免费工具让你的电脑重获新生

Windows 11终极优化指南:使用Win11Debloat免费工具让你的电脑重获新生

Windows 11终极优化指南:使用Win11Debloat免费工具让你的电脑重获新生 【免费下载链接】Win11Debloat A simple, lightweight PowerShell script that allows you to remove pre-installed apps, disable telemetry, as well as perform various other changes to d…

2026/7/25 23:02:05 阅读更多 →
国产AI多轮对话按主题导出:Markdown底稿、Word文档与Excel清单

国产AI多轮对话按主题导出:Markdown底稿、Word文档与Excel清单

国产AI多轮对话按主题导出:Markdown底稿、Word文档与Excel清单**一句话答案:** DeepSeek、豆包、Kimi、通义千问、腾讯元宝这类中国 AI 的多轮对话,越聊越长以后,建议按“主题、项目、日期、用途”先分组,再用 DS随心转…

2026/7/25 23:01:05 阅读更多 →
Claude Code Skill 完整指南:从概念到实战创建自定义开发技能

Claude Code Skill 完整指南:从概念到实战创建自定义开发技能

最近在尝试使用 Claude Code 进行开发时,发现其内置的 Skill 功能非常强大,但官方文档比较零散,社区里关于如何定义、安装、创建和触发 Skill 的完整教程也不多。很多开发者,尤其是刚接触 Claude Code 的朋友,往往只停留在基础代码补全和对话上,没能充分利用 Skill 来大幅…

2026/7/25 23:01:05 阅读更多 →
从文档摘要到代码生成:GenAIExamples十大实用案例场景全攻略

从文档摘要到代码生成:GenAIExamples十大实用案例场景全攻略

从文档摘要到代码生成:GenAIExamples十大实用案例场景全攻略 【免费下载链接】GenAIExamples Generative AI Examples is a collection of GenAI examples such as ChatQnA, Copilot, which illustrate the pipeline capabilities of the Open Platform for Enterpr…

2026/7/25 23:01:05 阅读更多 →

日新闻

突破文档下载限制:kill-doc让你看到的都能保存

突破文档下载限制:kill-doc让你看到的都能保存

突破文档下载限制:kill-doc让你看到的都能保存 【免费下载链接】kill-doc 看到经常有小伙伴们需要下载一些免费文档,但是相关网站浏览体验不好各种广告,各种登录验证,需要很多步骤才能下载文档,该脚本就是为了解决您的…

2026/7/25 0:00:35 阅读更多 →
C++ string类模拟实现:从深拷贝到内存管理的完整指南

C++ string类模拟实现:从深拷贝到内存管理的完整指南

1. 项目概述:为什么我们要“手撕”string类?在C的学习道路上,尤其是从C语言过渡到C的“初阶”阶段,string类绝对是一个绕不开的核心。标准库里的std::string用起来太方便了,、find、substr,几个操作符和函数…

2026/7/25 0:00:35 阅读更多 →
三角洲寻宝鼠工具:高效文件搜索与资源管理实战指南

三角洲寻宝鼠工具:高效文件搜索与资源管理实战指南

1. 先搞清楚“三角洲寻宝鼠”到底是什么工具从名称来看,“三角洲寻宝鼠”更像是一个资源查找或文件检索类工具,而不是游戏或娱乐软件。这类工具的核心价值在于帮助用户快速定位特定资源,比如文档、图片、压缩包或特定格式的文件。如果你经常需…

2026/7/25 0:00:35 阅读更多 →

周新闻

Go语言静态资源打包方案对比与实践指南

Go语言静态资源打包方案对比与实践指南

1. 项目背景与核心需求在Go语言开发中,我们经常需要处理静态资源文件的打包问题。无论是Web应用的模板文件、前端资源,还是配置文件、证书等,都需要随程序一起分发。传统做法是将这些文件与编译后的二进制文件放在同一目录下,但这…

2026/7/25 5:08:22 阅读更多 →
Go语言实现高性能LDAP认证服务的架构与实践

Go语言实现高性能LDAP认证服务的架构与实践

1. 项目背景与核心价值LDAP(轻量级目录访问协议)作为企业级身份认证的黄金标准,已经服务了超过80%的财富500强公司。我在金融科技领域实施统一认证体系时,发现传统Java方案存在启动慢、内存占用高等痛点。而Go语言凭借其协程并发模…

2026/7/25 5:13:53 阅读更多 →
【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

更多请点击: https://intelliparadigm.com 第一章:AI面试官实战指南的核心价值与适用场景 AI面试官并非替代人类HR的“黑箱工具”,而是以可解释、可审计、可迭代的方式,赋能招聘全链路的关键基础设施。其核心价值在于将主观经验沉…

2026/7/24 18:52:18 阅读更多 →

月新闻