
简介这是一款面向Java开发者和系统运维人员的线程转储Thread Dump分析工具提供Web界面用于远程诊断死锁、线程阻塞等并发性能问题。项目采用前后端分离架构既可用于生产环境问题排查也适合作为学习JVM线程机制与并发编程的实践案例。资源共81个文件、压缩包约1.49MB其中包含25个TypeScript、9个Java、9个JSON、8个CSS及8个HTML等文件前端基于Angular实现界面交互后端基于Maven构建并提供线程转储解析接口目录划分清晰便于按模块阅读。已有116人学习适合需要掌握线程状态分析、死锁检测及Web化运维工具的Java开发者。包内除完整前后端源码外还保留了pom.xml、mvnw、angular.json等构建配置与说明文档可快速搭建运行环境深入理解Java并发API、JVM线程管理及日志监控在真实场景中的应用。1. Thread_Dump_Analyzing_Tool 到底在解决什么问题从原始快照到故障结论线上接口突然从 20ms 涨到 2 秒或者进程深夜悄悄假死Java 工程师的第一反应基本都是抓一份线程 dump。但 dump 到手那一刻问题才真正开始——几百个线程、几千行栈帧眼睛根本看不过来。Thread_Dump_Analyzing_Tool 这类工具的价值就是把原始快照变成结论统计线程状态分布、检测死锁环、定位热点线程让排障从「人肉翻栈」变成「看报告」。下面按我自己做这类工具的流程来讲。先讲清 dump 里每个字段意味着什么因为工具的价值上限由你对输入数据的理解决定再给出一套可照抄的最小 Python 实现覆盖解析、死锁检测和状态统计最后列出线上环境真踩过的坑多数误报不是算法不行而是挂在输入数据的小细节上。适合谁读被线上 dump 折磨过的后端开发和运维以及想写内部诊断工具但不想从零试错的人。2. 抓对原始线程 dumpjstack 与 jcmd 的最小用法和字段解读线程 dump 分析工具的第一原则垃圾进垃圾出。我一开始就吃过亏——用不带-l的 jstack 抓数据解析器跑完一片正常死锁检测却永远返回空。后来才明白锁信息根本没被抓下来。所以这一章先把「喂给工具的数据」讲透怎么抓、抓成什么样、哪些字段是关键。2.1 抓 dump 的三个命令jstack、jcmd、kill -3 怎么选最常见的抓取命令是jstack -l pid但不同 JDK 版本、不同权限环境下可用命令不一样。我一般按下面的顺序判断# 找到目标 Java 进程jps 比 ps 更精准 jps -l # 本机 attach 权限正常时首选 jcmdJDK 8 起内置 jcmd pid Thread.print -l dump_1.txt # 工具链不完整时退回 jstack注意也要带 -l jstack -l pid dump_1.txt # 容器/生产环境没有 attach 权限时用 kill -3输出进 stdout 日志 kill -3 pid-l是关键参数它把每个线程持有的锁locked ...和正在等待的锁waiting to lock ...打出来。没有这些行后面的死锁检测全是空谈工具再漂亮也白搭。kill -3不是杀进程而是向 JVM 发 SIGQUIT 信号JVM 收到后把线程 dump 写到标准输出生产环境通常配合 nohup 或日志重定向用。注意kill -3的 dump 输出到进程标准输出不一定落在当前终端。生产环境里 stdout 通常被日志框架重定向抓完去对应日志文件里搜Full thread dump确认落盘位置。另外jcmd的输出格式和jstack在某些版本有细微差异锁行的缩进、空行数量不完全一致。解析器最好写成容错的正则匹配而不是严格对齐空格否则换个 JDK 版本就翻车。2.2 一份 dump 里的字段地图线程名、nid、状态与锁行拿到 dump 先别急着丢给工具花两分钟看一段原始输出后面写解析器会顺很多。下面是一段有代表性的线程记录http-nio-8080-exec-7 #25 daemon prio5 os_prio0 cpu8.75ms elapsed96.2s tid0x00007f3cc012d800 nid0x1f3b runnable [0x00007f3cc05e7000] java.lang.Thread.State: RUNNABLE at java.net.SocketInputStream.socketRead0(Native Method) at java.net.SocketInputStream.socketRead(SocketInputStream.java:110) at com.example.web.OrderController.getOrder(OrderController.java:42) - waiting to lock 0x000000074e8038d0 (a java.lang.Object)第一行是线程头双引号里是线程名nid0x1f3b是操作系统原生线程 ID 的十六进制表示tid是 JVM 内部线程 ID中括号里是线程栈指针。第二行java.lang.Thread.State: RUNNABLE是 JVM 线程状态这是后面所有统计的基准。- waiting to lock 0x...表示这个线程正在阻塞等待某把锁如果看到- locked 0x...表示它已经持有这把锁。容易被忽略的是cpu8.75ms elapsed96.2s部分较新版本 JDK 的 dump 第一行会带这两个数字线程累计 CPU 消耗和存活时长对定位热点非常有用。没有这行也没关系后面 3.3 给降级方案。2.3 四种核心状态怎么解读RUNNABLE、BLOCKED、WAITING、TIMED_WAITING如果把 dump 比作体检报告线程状态就是生命体征。下面是四种高频状态在分析中的含义我一般把它们映射成三类结论干活、等待、卡死。状态含义分析结论倾向RUNNABLE线程正在执行指令可能在业务代码也可能在 native 调用结合栈顶帧判断是正常执行还是 CPU 热点BLOCKED线程在等一把被其它线程持有的锁进入 synchronized 块失败锁竞争信号必须找持锁线程WAITING线程主动等待 notify 或 LockSupport 唤醒可能是池化空闲也可能是业务死等TIMED_WAITING带超时的等待sleep、wait(timeout)、parkNanos多数正常关注超时时间是否异常最容易翻车的判断是「WAITING 等于出问题」。一个健康的 Spring Boot 服务线程池里有几十个线程长期停在 WAITING / TIMED_WAITING栈顶是ThreadPoolExecutor.getTask或LockSupport.park这是正常池化空闲。真正要警惕的是栈顶停在业务代码Object.wait()上、长时间不返回——那是黑匣子故障notify 一旦丢失就是真死等。3. 从零写一个线程 dump 分析工具解析器、死锁检测与报告三件套这一章直接给可抄的作业。实现语言选 Python 3原因有三个正则处理和文本遍历快dataclass让记录结构清晰输出报表不需要任何重量级依赖。整个工具拆三个模块解析器把文本变结构分析器找死锁算热点报告器打印结论。把三段代码拼成一个analyzer.py就能跑。3.1 解析器用正则把线程块拆成结构化记录解析的核心是识别「一个线程从哪里开始、到哪里结束」。线程头行以开头包含nid0x...结尾是[...]栈指针之后所有缩进行都属于这个线程直到下一个线程头。基于这个规律写一个按行扫描的状态机解析器import re from dataclasses import dataclass, field dataclass class ThreadRecord: name: str nid: str # 原生线程 ID十六进制字符串 state: str UNKNOWN stack: list field(default_factorylist) locks_held: list field(default_factorylist) # - locked ... locks_waiting: list field(default_factorylist) # - waiting to lock ... HEAD_RE re.compile(r^(.?).*?nid0x([0-9a-f]).*?\[.*?\]$) STATE_RE re.compile(r^\sjava\.lang\.Thread\.State:\s(\w)) STACK_RE re.compile(r^\sat\s(.)$) LOCKED_RE re.compile(r^\s- locked (0x[0-9a-f])) WAITING_RE re.compile(r^\s- waiting to lock (0x[0-9a-f])) def parse_dump(path): records [] cur None with open(path, encodingutf-8, errorsreplace) as f: for line in f: head HEAD_RE.match(line) if head: cur ThreadRecord(namehead.group(1), nidhead.group(2)) records.append(cur) continue if cur is None: continue m_stack STACK_RE.match(line) if m_stack: cur.stack.append(m_stack.group(1)) continue m_state STATE_RE.match(line) if m_state: cur.state m_state.group(1) continue m_lock LOCKED_RE.match(line) if m_lock: cur.locks_held.append(m_lock.group(1)) continue m_wait WAITING_RE.match(line) if m_wait: cur.locks_waiting.append(m_wait.group(1)) return records几个参数说明HEAD_RE末尾的\[.*?\]$保证只匹配真正的线程头避免把栈帧里的at行误当成新线程nid保留十六进制原始字符串千万别在解析阶段转成十进制后面要对账时再按需转换。锁行正则只取地址0x...因为死锁检测只依赖地址相等性不关心锁的类名。errorsreplace处理 dump 里可能出现的非法 UTF-8 字节——中文日志混入时常见不加这一项解析到一半就抛异常了。3.2 死锁检测把锁等待关系画成图再找环死锁的数学本质是「锁等待图里有环」线程 A 持有锁 1 等待锁 2线程 B 持有锁 2 等待锁 1A、B 互相卡死。实现分两步先建立「锁地址 → 持有线程」的映射再根据waiting to lock建线程间的等待边最后 DFS 找环。def find_deadlock_cycles(records): # 锁地址 - 持有该锁的线程这里用 nid 做唯一标识 holder {} for r in records: for lock in r.locks_held: holder[lock] r # 等待图节点用 nid线程池同名线程不会互相误并 graph {} name_of {} for r in records: name_of[r.nid] r.name for lock in r.locks_waiting: h holder.get(lock) if h is not None and h.nid ! r.nid: graph.setdefault(r.nid, []).append(h.nid) cycles [] visited, path set(), [] def dfs(nid): visited.add(nid) path.append(nid) for nxt in graph.get(nid, []): if nxt in path: i path.index(nxt) cycle path[i:] [nxt] cycles.append([name_of[n] for n in cycle]) elif nxt not in visited: dfs(nxt) path.pop() for nid in graph: if nid not in visited: dfs(nid) return cycles这段代码就是三色 DFSpath是当前深搜路径上的线程 nid如果下一步要去的 nid 已出现在path里说明找到了环visited防止重复遍历path.pop()保证回溯到上一个分支点。两个注意点第一holder映射依赖锁地址完整0x前缀一旦丢或地址被截断死锁检测必然漏报第二同线程重入同一把锁会在locks_held里出现重复地址所以分析前应该去重——下面 4.4 会细说。3.3 状态统计与热点线程把 dump 变成一张一眼能看懂的表死锁检测解决「卡死」热点分析解决「慢」。没有cpu字段时热点判断靠多份 dump 交叉验证同一线程名如果多次出现在 RUNNABLE、且前几个栈帧落在业务代码包名里大概率在烧 CPU。代码先按状态聚合成占比表再对多次 dump 统计 RUNNABLE 线程名频次from collections import Counter def summarize(records): cnt Counter(r.state for r in records) total len(records) or 1 print(f{state:18}{count:6}{percent:9}) for state, n in cnt.most_common(): print(f{state:18}{n:6}{n * 100 // total:8}%) def hot_threads(all_records, top_n5): # all_records 是多次 dump 解析结果的列表定位反复 RUNNABLE 的线程 runnable_names Counter() for records in all_records: for r in records: if r.state RUNNABLE: runnable_names[r.name] 1 return runnable_names.most_common(top_n)hot_threads需要你手动把多次解析结果传进来这呼应了上一章说的原则单份 dump 是瞬时快照不能当结论。最后在主函数里串起来解析、找环、统计、打印。工具本身很小但解析规则和检测逻辑跟商业级线程分析工具是同一条思路——先结构化再找关系最后呈现。4. 线程 dump 分析工具的避坑实录五个让结果失真的常见问题工具写完只是开始。我跑了半年发现大多数误报漏报不是算法问题而是输入数据理解和抓取姿势不对。这五个坑按出现频率排序每条按「现象 → 原因 → 解决」讲方便对照排查。4.1 现象nid 对不上操作系统线程 PID热点定位落空抓完 dump用top -H -p pid看到 CPU 最高的线程号回 dump 里找对应线程怎么都对不上。原因很简单内核和 top 用十进制 PIDdump 里的nid是十六进制两套进制没换算。解决方法是先转进制再搜# 把 top -H 里最高的线程号假设是 8003转成十六进制 printf %x\n 8003输出1f43去 dump 里搜nid0x1f43就是它。反过来从 dump 拿 nid 想确认内核线程用echo $((16#1f43))。这个坑几乎每个新手必踩一次解决成本却极低建议直接写进工具的手册里。4.2 现象WAITING 线程一大堆工具告警「线程池异常」第一版工具跑完状态统计里 WAITING 占 40%半夜把运维叫起来。后来一查凌晨低峰期 Tomcat 线程池和业务异步线程都在正常池化等待。原因是我把「WAITING」等同「出问题」没看栈顶帧。解决方法是给分析器加规则当线程处于 WAITING/TIMED_WAITING 且栈顶是ThreadPoolExecutor.getTask、LockSupport.park、ForkJoinPool.awaitWork这类基础设施帧时归为「正常空闲」只有栈顶是业务代码才算「可疑阻塞」。这行判断逻辑虽简单能把误报率从 40% 打到接近零。4.3 现象同一个死锁环时有时无检测结果不稳定连续抓五份 dump死锁检测只报出两次同一个环。开始我以为是算法随机性后来想明白dump 是瞬时快照线程在「等锁」和「抢到锁被调度」之间切换某个瞬间锁被放下环就断裂了。解决方法是把判定条件从「一份 dump 里有环」改成「N 份里至少 M 份检出同一个环」通常取 N5、M3抓取间隔拉到 3 秒以上避免连续快照采样到同一状态造成假阳性。4.4 现象线程「持有锁 A 等锁 B」时工具在它自己身上报了个环这是最隐蔽的坑。一个线程栈里同时出现locked A和waiting to lock B正好是死锁环的典型形态但 synchronized 可重入同一线程对同一把锁 A 可能有多条locked记录如果不做处理「自己持有 A、自己又等待 A」会被误判成自环。原因就是没区分「持有」和「等待」两个集合也没对锁地址去重。解决方法是locks_held按线程内去重同一地址只保留一条建等待图时强制h.nid ! r.nid排除自环。另外提醒一句锁地址解析必须取完整 12 位以上十六进制截断会导致不同锁碰撞holder 映射全错。4.5 现象老版本 JDK 的 dump 没有 cpu 字段热点线程全部落空JDK 8 的 jstack 输出里没有cpu和elapsed我写的热点分析依赖这个字段结果老版本环境跑出来一片空白。解决方法是降级检测到 dump 里没有 cpu 字段时自动切到「多份 dump 交叉统计」模式——抓 5 份间隔 3 秒的 dump统计线程名出现在 RUNNABLE 状态的次数按频次排序。虽然不如 cpu 精确但定位「某个线程池忙个不停」已经足够。还要记得jcmd 在部分版本里Thread.print默认输出也不带 cpu先确认目标环境的 JDK 行为再决定抓取命令。5. 让分析工具在线上站住脚五连抓验证法与下一步扩展工具跑通只是第一步难的是确认它没在骗你。我的习惯是造一份「自杀式」测试写一个 Java 程序两个线程互相持有对方需要的锁第三个线程跑死循环烧 CPU跑起来后抓五份 dump 喂给工具。这个验证比单元测试实在因为它用的是 JVM 真实产出的 dump格式兼容性和分析逻辑一起验了。# 测试机抓 5 份间隔 3 秒的 dump for i in 1 2 3 4 5; do jcmd $PID Thread.print -l dump_$i.txt sleep 3 done # 跑工具核对死锁环与热点线程 python3 analyzer.py dump_*.txt --states --deadlock验证要点有两个死锁环应该连续三份以上出现且线程名、锁地址能对上测试代码热点线程里必然包含烧 CPU 的那个线程。如果你加了自动告警先在测试机验证它不误报正常环境。验证通过后把抓取脚本交给定时任务每 10 分钟抓一次把状态占比、死锁结果推到告警平台。线上抓 dump 本身有开销STW 变长、GC 暂停被拉长频率别太低我的血泪经验是 10 分钟一次以下并且用jcmd而不是jstack减少进程外 attach 的隐患。后续扩展方向也清晰把多份 dump 的状态占比做成时序曲线能看到「线程池慢慢被占满」的渐变故障再做线程池名归并把线程名只差数字的池合并统计误报又能降一档。每接入一个新环境我先拿一份历史 dump 对一遍输出确认格式符合预期再放开。希望帮到你。本文还有配套的精品资源点击获取