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

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

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

2026/10/24 19:20:33 阅读更多 →

最新新闻

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

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

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

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

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

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

2026/10/24 17:47:00 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

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

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

2026/10/24 18:34:36 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

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

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

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

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

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

2026/10/25 3:12:30 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

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

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

2026/10/24 21:09:59 阅读更多 →

日新闻

周新闻

月新闻

我发现了一个新思路:用 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/24 9:15:56 阅读更多 →
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/24 11:09:35 阅读更多 →
黑夜航拍船只数据集训练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/24 5:28:28 阅读更多 →