线程转储分析器
从线程转储中诊断并发问题。找到死锁,识别争用热点,检测线程池耗尽,并映射阻塞/等待线程链 —— 跨JVM、Go、Python和Node.js运行时。
使用时: "分析线程转储"、"应用程序挂起"、"检测到死锁"、"线程卡住"、"goroutine泄漏"、"连接池耗尽"、"线程池满"、"应用程序无响应",或应用程序停止处理请求时。
命令
步骤1:捕获线程转储
JVM(Java/Kotlin/Scala):
# 方法1:jstack
jstack $(pgrep -f 'java.
your-app') > /tmp/thread-dump.txt 2>&1
# 方法2:kill -3(打印到stdout/stderr)
kill -3 $(pgrep -f 'java.your-app')
# 方法3:jcmd
jcmd $(pgrep -f 'java.
your-app') Thread.print > /tmp/thread-dump.txt
Go:
# 方法1:SIGQUIT(打印所有goroutine到stderr)
kill -QUIT $(pgrep -f 'your-go-app')
# 方法2:pprof端点
curl -s http://localhost:6060/debug/pprof/goroutine?debug=2 > /tmp/goroutine-dump.txt
# 方法3:runtime.Stack()
curl -s http://localhost:6060/debug/pprof/goroutine?debug=1 | head -50
Python:
# 方法1:faulthandler
kill -USR1 $(pgrep -f 'python.your-app')
# 方法2:程序化
python3 -c " import threading, traceback, sys
for thread_id, frame in sys._current_frames().items():
thread = next((t for t in threading.enumerate() if t.ident == thread_id), None)
name = thread.name if thread else 'Unknown'
print(f'\n--- Thread {name} ({thread_id}) ---')
traceback.print_stack(frame) "
Node.js:
# 异步堆栈跟踪
kill -USR1 $(pgrep -f 'node.*your-app')
# 通过chrome://inspect连接并捕获
步骤2:解析线程状态
对每个线程/goroutine进行分类:
状态 JVM Go 含义
RUNNABLE RUNNABLE 运行中 主动执行
WAITING WAITING/TIMED_WAITING 等待信号
BLOCKED BLOCKED 等待锁
PARKED PARKING 睡眠 等待I/O
按状态统计线程:
RUNNABLE:15(18%)
WAITING:45(54%)
BLOCKED:12(14%)
如果BLOCKED > 10% → 可能存在争用问题。
步骤3:检测死锁
JVM:在转储输出中查找"Found one Java-level deadlock"。
手动检测(任何语言):
找到所有BLOCKED线程和它们等待的锁
找到谁持有每个锁
检查循环:线程A等待锁1,由线程B持有,线程B等待锁2,由线程A持有
死锁检测:线程"worker-1" BLOCKED等待锁@0x7f3a(由"worker-3"持有)
线程"worker-3" BLOCKED等待锁@0x8b2c(由"worker-1"持有)
→ 循环依赖:worker-1 → Lock@0x7f3a → worker-3 → Lock@0x8b2c → worker-1
步骤4:检测线程池耗尽
线程池"http-handler" —— 200/200线程(100%利用率)
180个线程BLOCKED在数据库连接池上
15个线程WAITING在外部API响应上
5个线程RUNNABLE(处理中)
线程池耗尽:所有处理线程都被占用
根因:数据库连接池是瓶颈
→ 连接池最大:20,所有20个都在使用
→ 平均查询时间:2.3s(正常50ms)
→ 可能是慢查询或锁争用
步骤5:识别争用热点
按等待的锁对阻塞线程进行分组:
锁争用热点
锁:DatabaseConnectionPool@0x7f3a —— 85个线程等待
- 持有者:线程"worker-42"(执行SQL查询15秒)
- 等待者:85个线程阻塞,平均8.3秒
- 影响:所有传入请求排队
- 修复:调查worker-42中的慢查询,增加池大小
锁:CacheManager@0x8b2c —— 12个线程等待
- 持有者:线程"cache-refresh-1"(从DB加载缓存)
- 等待者:12个线程阻塞,平均1.2秒
- 影响:缓存读取在刷新期间阻塞
- 修复:使用读写锁或缓存刷新的双缓冲
步骤6:生成报告
# 线程转储分析
摘要
- 总线程数:215
- 死锁:0
- 线程池利用率:100%(耗尽)
- 顶级争用:DatabaseConnectionPool(85个阻塞线程)
- 可能的根因:慢查询占用所有DB连接
线程状态分布
- RUNNABLE:15(7%)
- BLOCKED:97(45%)
- WAITING:85(40%)
- TIMED_WAITING:18(8%)
根因链
- 慢SQL查询(15s)占用连接在worker-42中
- 连接池(最大20)耗尽 —— 所有连接繁忙
- 85个HTTP处理线程阻塞等待连接
- 线程池(200)耗尽 —— 所有线程被占用
- 新请求被拒绝,返回503
推荐
- 立即:杀死慢查询并调查(可能缺少索引)
- 短期:添加查询超时(5s最大)
- 中期:增加连接池到50
- 长期:在DB访问上添加断路器
- compare —— 比较两个线程转储
在不同时间捕获转储并突出显示:
新阻塞线程
线程卡在同一状态(没有进展)
线程数量增长(泄漏)
争用模式变化