尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

生产环境死锁排查:从线程堆栈到锁等待图的完整分析链路

生产环境死锁排查:从线程堆栈到锁等待图的完整分析链路
📅 发布时间:2026/7/19 18:11:53

生产环境死锁排查:从线程堆栈到锁等待图的完整分析链路

一、所有请求线程突然卡住不动,日志一天没打一行

生产环境最诡异的故障之一:服务还活着,端口还开着,健康检查还能过,但所有请求都超时。查 CPU 使用率——几乎为零。查内存——正常范围。查数据库连接池——有很多空闲。但就是不响应任何请求。这种症状在 Java 应用中十有八九是发生了死锁。

死锁的根源很简单:两个或多个线程相互等待对方释放锁资源,形成闭环等待,谁也走不出去。但排查死锁的过程不是谁都能靠直觉完成的——必须从现象出发,沿着数据收集、堆栈分析、锁等待图构建的链路一步步走,最终定位到是哪两个线程在哪两把锁上互相卡住。

排查死锁的完整链路如下:

  1. 发现异常(请求超时、CPU空闲)后,先收集第一手数据:使用jstack导出线程堆栈,同时查看GC日志排除GC停顿,检查数据库连接排除DB层死锁。
  2. 拿到线程堆栈后,定位所有处于BLOCKED状态的线程,提取每个线程等待的锁地址,构建锁等待有向图。
  3. 检查图中是否存在环:若存在环,即可确认死锁,定位环中的线程和锁;若不存在环,则属于非死锁问题,需排查其他原因。
  4. 确认死锁后,分析死锁的业务逻辑,通过排序锁获取顺序等方式修复,最后验证修复并上线。

二、从 jstack 中读取死锁信息

在 Java 中排查死锁的第一步是jstack <pid>。这条命令会打印所有线程的当前堆栈,并自动检测死锁。如果能直接看到 JVM 打印的Found one Java-level deadlock信息,恭喜你,死锁已经被 JVM 自动检测出来了。

但有些不那么"经典"的死锁,JVM 可能检测不到——比如通过CountDownLatch、Semaphore或线程池造成的逻辑死锁,这些不涉及 JVM 锁监视器,jstack 不会主动报告。这种情况下需要手动分析线程堆栈。

以下是两个线程互相等待的典型堆栈片段:

"Thread-A" #23 prio=5 os_prio=0 tid=0x00007f8a0c001000 nid=0x5a3b 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 prio=5 os_prio=0 tid=0x00007f8a0c001800 nid=0x5a3c 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,等待 LockA;Thread-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 List<List<String>> detectDeadlocks(String jstackOutput) { // 构建锁等待图 Map<String, Set<String>> waitGraph = buildWaitGraph(jstackOutput); // 在有向图中使用 DFS 检测环 return findCycles(waitGraph); } /** * 从 jstack 文本中构建锁等待关系 */ private Map<String, Set<String>> buildWaitGraph(String jstackOutput) { Map<String, Set<String>> graph = new HashMap<>(); // 按线程分段 String[] threadBlocks = jstackOutput.split("\n\n"); for (String block : threadBlocks) { if (!block.contains("BLOCKED")) { continue; // 只关注等待锁的线程 } // 解析 locked(该线程持有的锁) Set<String> 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 List<List<String>> findCycles(Map<String, Set<String>> graph) { List<List<String>> cycles = new ArrayList<>(); Set<String> whiteSet = new HashSet<>(graph.keySet()); Set<String> graySet = new HashSet<>(); Set<String> blackSet = new HashSet<>(); while (!whiteSet.isEmpty()) { String start = whiteSet.iterator().next(); List<String> currentPath = new ArrayList<>(); dfs(start, graph, whiteSet, graySet, blackSet, currentPath, cycles); } return cycles; } private boolean dfs(String current, Map<String, Set<String>> graph, Set<String> white, Set<String> gray, Set<String> black, List<String> path, List<List<String>> 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); List<String> 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 加超时是工程解,缩小锁粒度是架构解。三者可以根据场景组合使用。最重要的是:不要在代码评审时觉得"这种加锁方式一般不会出问题"——只要存在环状等待的可能性,死锁就一定会发生,只是时间早晚的问题。

相关新闻

  • SplinterDB与传统存储对比:为什么它是NVMe设备的理想选择?[特殊字符]
  • KnpGaufretteBundle多适配器配置实战:本地、FTP、SFTP、云存储统一管理
  • 家庭服务器从入门到精通 · 第 6 篇 · NPM代理到家庭首页导航中心全自动化智能化实现

最新新闻

  • 广州荔湾区正规废旧物资回收哪家靠谱?广州浩宇物资 24 小时上门收电脑 / 服务器 / 空调 / 灭火器 - 星际AI
  • 长沙劳力士回收价格查询与各大平台实测排行(2026年7月最新) - 天价名表回收平台
  • 欧米茄官方更换表蒙价格查询|详细地址与维修热线权威信息公告(2026年7月最新) - 欧米茄服务中心
  • 2026陕西省企业参考:支持语义分析的GEO优化工具核心价值及与SEO差异 - 企智芯
  • 2026年7月最新劳力士香港官方售后网点地址公示+官方客户服务电话 - 劳力士官方服务中心
  • 2026年7月最新哈尔滨松北区松浦街道亨得利官方钟表服务中心电话公示 - 亨得利官方博客

日新闻

  • 百达翡丽官方服务项目及价格查询|维修地址与电话权威信息通告(2026年7月最新) - 百达翡丽服务中心
  • 2026年药食同源冲泡饮品哪家好:衡身堂三伏天内调外养 - 晚香时候
  • 芝柏官方更换原装表带价格查询|详细地址与24小时客服电话权威信息公告(2026年7月最新) - 亨得利官方服务中心

周新闻

  • SaaS软件行业GEO实践:AI搜索时代的品牌可见性与获客新路径
  • 什么是PCTFE?医药高端包装的“防潮王牌“材料
  • 【JVM调优实战】16-可视化利器-JConsole-VisualVM-JMC

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号