Jstack定位生产环境线程阻塞问题解决

📅 2026/7/6 4:04:48
Jstack定位生产环境线程阻塞问题解决
一、问题现象告警现象有一个异步处理的任务日志没有往后继续打印了这个任务对应的队列存在消息积压系统日志无任何ERROR堆栈服务既不崩溃也不恢复第一反应服务没挂但卡住了极大概率是线程阻塞问题。二、初步诊断jstack第一眼看到什么2.1 获取Java进程PID# 找到拓扑服务的进程ID ps aux | grep topology | grep -v grep # 结果PID234562.2 生成第一次线程堆栈jstack -l 23456 thread_dump_1.log2.3 统计线程状态分布快速判断问题类型grep java.lang.Thread.State thread_dump_1.log | sort | uniq -c结果87 java.lang.Thread.State: RUNNABLE 23 java.lang.Thread.State: TIMED_WAITING 15 java.lang.Thread.State: WAITING ← 异常有15个线程在无限期等待 2 java.lang.Thread.State: BLOCKED初步判断15个WAITING状态的线程不正常需要深入分析。三、核心排查逐层分析问题线程3.1 查找所有WAITING状态的线程grep -B 5 WAITING thread_dump_1.log | grep PE_EVT -A 10发现目标线程PE_EVT1-1 #63 prio5 tid0x00007feffa0e7800 nid0x68 waiting on condition java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for 0x00000006cc4cb2f8 (a java.util.concurrent.locks.StampedLock) at java.util.concurrent.locks.StampedLock.acquireWrite(StampedLock.java:1119) at java.util.concurrent.locks.StampedLock.writeLock(StampedLock.java:354) at com.tethrnet.terra.service.sr.topology.lib.api.model.NetworkGraph.addVertex(NetworkGraph.java:27) at com.tethrnet.terra.service.sr.server.topology.service.TopologyManager.updateNode(TopologyManager.java:553) at com.tethrnet.terra.service.sr.server.topology.service.TopologyManager.removePeInfos(TopologyManager.java:1929) at com.tethrnet.terra.service.sr.server.topology.service.ProdTopologyManager.process(ProdTopologyManager.java:294) at com.tethrnet.terra.service.sr.server.topology.service.ProdTopologyManager.processPeEvent(ProdTopologyManager.java:151)3.2 解读这个堆栈的关键信息第一层线程状态WAITING (parking)线程被挂起了在等某个条件被满足才能继续第二层在等什么parking to wait for 0x00000006cc4cb2f8 (StampedLock)在等一把读写锁StampedLock.writeLock()具体是在等写锁第三层业务在做什么TopologyManager.removePeInfos在处理PE设备删除操作NetworkGraph.addVertex正在向内存拓扑图中添加顶点疑问来了删除PE信息为啥在添加顶点这需要看代码逻辑但暂时先不管继续追锁问题。3.3 找到锁的占用者关键一步在完整的堆栈文件中搜索这把锁grep -B 15 -A 30 0x00000006cc4cb2f8 thread_dump_1.log找到占用线程Topology-Processor-5 #120 prio5 tid0x00007feffa123800 nid0x90 runnable java.lang.Thread.State: RUNNABLE at java.net.SocketInputStream.socketRead0(Native Method) at java.net.SocketInputStream.read(SocketInputStream.java:129) at com.mysql.jdbc.MysqlIO.readFully(MysqlIO.java:3123) at com.mysql.jdbc.MysqlIO.nextRow(MysqlIO.java:2248) at com.mysql.jdbc.ResultSetImpl.next(ResultSetImpl.java:474) - locked 0x00000006cc4cb2f8 (a java.util.concurrent.locks.StampedLock) at com.tethrnet.terra.service.sr.topology.lib.api.model.NetworkGraph.rebuildIndex(NetworkGraph.java:89) at com.tethrnet.terra.service.sr.topology.lib.api.model.NetworkGraph.syncFromDB(NetworkGraph.java:156) at com.tethrnet.terra.service.sr.server.topology.service.TopologyManager.initGraph(TopologyManager.java:201)真相大白持有锁的线程在RUNNABLE状态它在执行数据库查询MysqlIO.readFully问题很明确在持有写锁的情况下执行了耗时的数据库操作四、确认问题持续时长连续采样4.1 多次采样判断是否是瞬时问题for i in 1 2 3; do echo 第${i}次采样 jstack -l 23456 thread_dump_${i}.log sleep 10 done4.2 对比三次结果# 检查PE_EVT1-1线程在三次采样中的状态 for i in 1 2 3; do echo dump_${i} grep -A 2 PE_EVT1-1 thread_dump_${i}.log | grep WAITING done结果 dump_1 java.lang.Thread.State: WAITING (parking) dump_2 java.lang.Thread.State: WAITING (parking) dump_3 java.lang.Thread.State: WAITING (parking)结论至少30秒内线程状态没有任何变化确认是持续阻塞不是瞬时抖动。五、根因定位代码层面分析5.1 问题代码定位通过堆栈信息锁定到具体代码行NetworkGraph.addVertex第27行NetworkGraph.rebuildIndex第89行查看代码发现问题// 问题代码NetworkGraph.java public void addVertex(Vertex v) { long stamp lock.writeLock(); // 第27行获取写锁 try { validateVertex(v); persistToDatabase(v); // ← 数据库操作在锁内 vertexMap.put(v.getId(), v); } finally { lock.unlockWrite(stamp); } } public void rebuildIndex() { long stamp lock.writeLock(); // 同样的问题 try { syncFromDB(); // ← 数据库同步在锁内 rebuildInvertedIndex(); } finally { lock.unlockWrite(stamp); } }根因确认写锁保护了内存操作但错误地包裹了数据库IO操作当数据库响应慢时锁被长时间占用其他所有需要写锁的线程包括PE事件处理全部被阻塞六、解决方案与验证6.1 代码修复// 修复后的代码 public void addVertex(Vertex v) { // 1. 耗时操作移到锁外 validateVertex(v); persistToDatabase(v); // ← 移出锁保护范围 // 2. 只在必要时加锁 long stamp lock.writeLock(); try { vertexMap.put(v.getId(), v); } finally { lock.unlockWrite(stamp); } } // 或者更好的方案使用并发容器 private final ConcurrentHashMapString, Vertex vertexMap new ConcurrentHashMap(); public void addVertex(Vertex v) { validateVertex(v); persistToDatabase(v); vertexMap.put(v.getId(), v); // ConcurrentHashMap内部处理并发 }6.2 修复后验证# 部署修复版本后再次观察线程状态 jstack -l 45678 thread_dump_fixed.log grep java.lang.Thread.State thread_dump_fixed.log | sort | uniq -c修复后结果105 java.lang.Thread.State: RUNNABLE 18 java.lang.Thread.State: TIMED_WAITING 0 java.lang.Thread.State: WAITING ← WAITING线程消失 0 java.lang.Thread.State: BLOCKED七、经验总结线程阻塞问题定位方法论7.1 完整排查流程图【现象】服务响应慢/假死 ↓ 1. 统计线程状态分布 grep Thread.State dump.log | sort | uniq -c → 大量WAITING/BLOCKED 锁竞争问题 ↓ 2. 定位问题线程 grep -B 5 WAITING dump.log | grep 业务关键字 → 找到卡住的业务线程 ↓ 3. 分析线程堆栈 - 看状态WAITING/BLOCKED/RUNNABLE - 看锁对象waiting to lock 地址 - 看业务代码定位到具体行号 ↓ 4. 找到锁占用者 grep -A 30 锁地址 dump.log → 找到持有锁的线程 ↓ 5. 确认问题持续时长 连续采样3次间隔10秒 → 对比线程状态是否变化 ↓ 6. 定位代码并修复 - 缩小锁范围 - 耗时操作移出锁外 - 使用并发容器替代手动锁7.2 核心排查技巧问题特征排查方向典型堆栈特征大量WAITING线程锁竞争找锁占用者waiting to lock 地址两个线程相互等待死锁jstack会明确提示Found one Java-level deadlock线程RUNNABLE但不动可能是死循环堆栈反复出现在同一行线程WAITING锁占用者是RUNNABLE锁内执行耗时操作锁占用者的堆栈中有IO/DB调用7.3 一次成功定位的关键点第一时间做线程dump问题出现时立即保存现场错过了可能就再也复现不了连续采样单次dump只能看到此时此刻多次才能判断持续多久寻找锁地址把waiting to lock的地址复制出来全文搜索找占用者关注业务代码不要只看框架堆栈一定要追到自己的业务代码行7.4 防止类似问题的编码原则// ❌ 错误锁内包含耗时操作 synchronized(lock) { databaseCall(); // 网络IO httpRequest(); // 网络IO fileWrite(); // 磁盘IO complexCalcuation(); // 大量计算 } // ✅ 正确只锁必要的最小范围 Object result expensiveOperation(); // 锁外执行 synchronized(lock) { sharedState.update(result); // 只锁状态修改 }八、写在最后这次排查让我深刻体会到jstack是Java开发者最锋利的瑞士军刀掌握它就能应对80%的线程问题不要被表象迷惑WAITING状态本身不是bug谁持有锁才是关键代码审查的盲区锁的范围往往是code review容易忽略的地方