简介:这是一款面向Java开发者和运维人员的线程转储分析工具,主要用于诊断应用响应慢、无响应等并发问题,通过Web界面上传并解析线程转储文件,快速识别死锁、线程阻塞及堆栈异常。压缩包内含81个文件,包体仅1.49MB,文件类型以TypeScript、Java、HTML、CSS、JavaScript、JSON为主:前端工程基于Angular,包含angular.json、tsconfig等配置;后端为Maven项目,含pom.xml、Java源码及配置文件,同时附有构建脚本、README和基础测试文件,目录结构清晰,便于按模块阅读和二次开发。已有115人学习下载。借助该工具,读者既能了解JVM线程状态、synchronized/Lock同步机制与死锁检测思路,也能观察一个完整Web工具从前端到后端的实现方式,包含线程转储解析、结果展示和并发排查实战案例,适合作为Java并发编程与故障诊断的学习参考。
1. Thread Dump 还没用起来?说明你的线上问题排查还停留在玄学阶段
Thread_Dump_Analyzing_Tool这个名字看起来像某个现成的分析工具,但真正干过一线排查的人都知道,它更像是一整套围绕线程转储(Thread Dump)的处理流程:抓取、解析、聚合、定位可疑线程。线上出现 CPU 飙高、接口超时、线程池打满这类问题,光看监控曲线只能确认「出事了」,Thread Dump 分析才是把「出事原因」从黑匣子里挖出来的那条路。这篇文章会把抓取命令、状态机解读、锁分析和避坑清单一起讲完,适合刚接手线上问题排查、或者被性能问题折腾过几回的 Java 应用开发者。
2. 先看懂 Thread Dump 再说分析:文件结构、生成方式与工具选型
很多人拿到 Thread Dump 的第一反应是打开文件硬读,读了两百行就放弃。这不是阅读能力的问题,是方法的问题。Thread Dump 本质上是 JVM 在某个瞬间对全部 Java 线程做的一次快照,里面包含了线程名、线程 ID、优先级、状态、调用栈、锁信息,以及 JNI 引用等内容。如果你想把它当小说读,那确实读不完;如果你把它当结构化数据来处理,它其实比大多数日志文件都干净得多。
2.1 一份标准的 Thread Dump 文件里到底写了什么
先看一个最典型的线程片段,这是排查时最常遇到的格式:
"http-nio-8080-exec-11" #24 daemon prio=5 os_prio=0 cpu=12.34ms elapsed=302.11s tid=0x00007f8a0c01a800 nid=0x2c3b waiting for monitor entry [0x00007f89ef7f4000] java.lang.Thread.State: BLOCKED (on object monitor) at com.example.order.service.OrderService.submit(OrderService.java:78) - waiting to lock <0x000000076e10a2d8> (a java.lang.Object) at com.example.order.controller.OrderController.create(OrderController.java:22) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:161)第一行是线程头信息,从左到右依次是:线程名、线程编号(#24)、是否是守护线程、优先级、操作系统优先级、CPU 消耗时间、存活时间、线程内存地址、本地线程 ID(nid)、线程状态描述和栈地址。其中最有用的是nid,它是操作系统层面的线程 ID,后面配合top -H查 CPU 占用时全靠它来对上号。
第二行的java.lang.Thread.State: BLOCKED (on object monitor)是 Java 层面的线程状态。往下是调用栈,从栈顶到栈底一帧一帧列出了当前执行位置。注意看- waiting to lock <0x...>这行,它表示这个线程正在等一把锁,锁的对象地址就是尖括号里的十六进制值。
整个 Thread Dump 文件的头部还会有一段 JVM 版本、操作系统架构、线程总数和死锁检测结果的信息。很多人忽略头部的死锁检测提示,实际上 JVM 在生成 Thread Dump 时会顺带做一次死锁检测,如果检测到死锁会直接输出Found one Java-level deadlock:这样的结论,这几乎是白送的信息。
2.2 生成 Thread Dump 的三种方式与适用场景
生成 Thread Dump 的方式有好几种,但适用场景完全不同。我按日常排查的使用频率排一下:
| 方式 | 命令/操作 | 适用场景 | 注意点 |
|---|---|---|---|
| jstack | jstack -l <pid> | 最通用,生产环境首选 | 需要 JDK 环境,进程用户权限要匹配 |
| kill -3 | kill -3 <pid> | 容器环境 / 没有 jstack 时 | 输出到 stdout,通常被重定向到日志文件 |
| jcmd | jcmd <pid> Thread.print | JDK 7+,功能更全 | 输出带详细锁信息,格式略不同 |
jstack -l里的-l参数很关键,它表示打印锁的详细信息,包括java.util.concurrent里的一些锁结构。没有-l的 dump 在分析并发问题时基本等于废的,因为你看不到waiting to lock和锁的持有关系。
容器环境里有个隐藏坑:你直接docker exec进容器执行 jstack,很可能会提示Unable to open socket file: target process not responding or HotSpot VM not loaded。这是因为容器里的 Java 进程可能运行在不同用户下,或者/tmp下的.java_pid<pid>文件没有权限访问。常见做法是加-F参数强制连接,或者用jcmd替代:
# 进入容器后找到 Java 进程 PID ps -ef | grep java # 用 jcmd 抓取 Thread Dump jcmd <pid> Thread.print -l > /tmp/thread_dump_$(date +%Y%m%d_%H%M%S).txtkill -3方式最适合那种连 JDK 都没有、只有 JRE 的瘦身镜像。信号发给进程后,JVM 会把 Thread Dump 写到标准输出,所以前提是你的 Java 进程 stdout 被重定向到了日志文件,否则在容器里可能直接丢到 Docker stdout 里找起来很费劲。我一般会把kill -3当作兜底方案,能上 jstack 就上 jstack。
2.3 分析工具怎么选:IDE、命令行还是在线解析
拿到 Thread Dump 文件后,选工具要看数据量。几十个线程的小应用直接文本搜索就够,几百个线程的微服务再靠肉眼就真不行了。
第一类工具是 IDE 插件,比如 VisualVM 的 Thread Dump 解析功能。它的优势是图形化展示线程状态分布和调用栈树,适合本地开发环境做初步排查。但生产环境的 dump 文件可能有一两千个线程,VisualVM 打开后渲染卡顿明显,而且它的解析逻辑偏「展示」,对锁关系的聚合分析并不深。
第二类是我个人最常用的方式:纯命令行。grep统计线程状态分布,awk按栈帧做聚合,把重复的调用栈合并成一行并计数。这种方式的优势是完全可控,规则自己写,不管是 200 还是 2000 个线程都能在几秒内得到一个聚合结果,而且不需要把敏感调用栈上传到任何外部服务。
第三类是在线解析工具。它们会自动识别被阻塞的线程、死锁链路和高频调用栈,输出一份很漂亮的报告。不过在线工具意味着要把堆栈文件传出去,如果你所在的项目对代码隐私有要求,这一条可以直接划掉。我自己的选择是:线上问题用命令行聚合快速定位,复杂死锁链路用在线工具的图形化展示来交叉验证,本地调试用 IDE 插件。
工具选型的核心原则只有一个:让「重复的栈」和「异常的锁」暴露出来,而不是追求可视化效果。下文第三节会把命令行聚合的最小实践完整跑一遍。
3. 用命令行完成一次 Thread Dump 分析:抓取、过滤与聚合的最小实践
工具选好之后,实际操作路径就清晰了:先抓取原始堆栈,再用命令行做状态统计,最后按栈帧聚合找出高频可疑线程。整个过程不依赖任何重型工具,一台能 SSH 到目标机器的工作机、一个 JDK 自带的jstack、再加一段几行的 shell 脚本就能完成,这也是Thread_Dump_Analyzing_Tool这个方向最朴素也最可靠的落地方式。
3.1 抓取 DUMP 和基础过滤的最低命令组合
先看一组最常用的命令组合,这三条命令我几乎每次排查都会用到:
# 1. 找到 Java 进程 PID,排除 grep 自身 ps -ef | grep java | grep -v grep # 2. 抓取线程快照,-l 输出详细的锁信息 jstack -l <pid> > dump_$(date +%Y%m%d_%H%M%S).txt # 3. 统计线程状态分布,先看全局再查细节 grep "java.lang.Thread.State" dump_*.txt | sort | uniq -c | sort -rn第一条命令是找进程,grep -v grep是为了把grep进程自己过滤掉,否则你可能会看到两个结果,浪费时间。第二条命令把 dump 输出到带时间戳的文件里,这个习惯很重要——后续做多份 dump 对比时,文件名里的时间戳能直接告诉你两次抓取之间的间隔。第三条命令统计所有线程的状态,输出像这样:
34 java.lang.Thread.State: RUNNABLE 17 java.lang.Thread.State: TIMED_WAITING (on object monitor) 12 java.lang.Thread.State: TIMED_WAITING (sleeping) 8 java.lang.Thread.State: WAITING (on object monitor) 5 java.lang.Thread.State: BLOCKED (on object monitor)这份分布就是整个排查的起点。如果 BLOCKED 线程数很少(个位数),问题大概率不在锁竞争上;如果 BLOCKED 数量占到了线程池核心线程数的一半以上,说明锁竞争已经很严重了,下一步就要针对阻塞线程的调用栈做聚合。
提示:抓 dump 时顺手再执行一次
grep -A 30 "BLOCKED"把阻塞线程的完整调用栈打印出来,不要等分析时才发现-l没加重进不了锁信息。
3.2 第一个结论怎么读:看到 BLOCKED 不等于找到根因
大多数人第一次拿到 Thread Dump,看到BLOCKED状态就兴奋,以为找到了元凶。实际上 BLOCKED 只是表象,它只告诉你「这个线程在等锁」,没告诉你「谁拿了锁不放」。真正的根因要找- waiting to lock后面的锁地址,然后在全量 dump 里搜索同一个地址,找到持有这把锁的线程。
聚合命令可以这样写:
# 找到所有等锁线程及其等待的锁地址 grep -B 1 "waiting to lock" dump_*.txt | grep -E 'Thread.State|waiting to lock' # 按锁地址聚合,统计每个锁被多少线程等待 grep "waiting to lock" dump_*.txt | awk '{print $NF}' | sort | uniq -c | sort -rn第二条命令的输出是锁地址和等待线程数的排序列表,排在前面的锁就是最热的锁。拿到锁地址后再回 dump 里搜:
grep -B 20 "<0x000000076e10a2d8>" dump_*.txt | grep -E 'Thread.State|locked'-B 20表示显示匹配行之前的 20 行,这样就能定位到在这把锁地址上有locked字样的持有者线程。这里有个很容易翻车的点:持有锁的线程往往不是 BLOCKED 状态,它可能正惬意地 RUNNABLE,或者在别的锁上 WAITING,所以直接按状态过滤会把真正的锁持有者漏掉。
3.3 按栈聚合:把 5000 行堆栈压成一张表格
如果线程数量上了 500,状态分布已经看不出太多信息了,这时要按调用栈做聚合。核心思路是把每个线程的主栈帧抽出来作为 key,统计相同调用栈的线程数量。
# 提取线程块,按第一个栈帧聚合统计 awk '/^"/{if(block!="")print block; block=$0; next} /^[[:space:]]+at /{if(block!=""){block=block" | "$1; }} /^$/{if(block!=""){print block; block=""}} END{if(block!="")print block}' dump_*.txt | sed 's/at //g' | sort | uniq -c | sort -rn | head -30这行命令的逻辑是:遇到以引号开头的线程头行时开启一个新的堆栈块,遇到at开头的栈帧时把它追加到当前块后面,遇到空行时输出当前块并清空。最后用sort | uniq -c做计数,按出现次数倒序排列。
输出会像这样:
23 "...-exec-" | com.example.service.PaymentService.refund | com.example.controller.PaymentController.refund | ... 9 "pool-3-thread-" | java.lang.Thread.sleep | java.util.concurrent.TimeUnit.sleep | ... 6 "...-exec-" | com.example.service.OrderService.cancel | ...看到 23 个线程都卡在PaymentService.refund时,答案基本就锁定了。这个聚合命令是我日常排查里使用频率最高的一段 shell,它把「哪些代码路径在被大量线程同时执行」这个问题变成了一个计数排序问题,比用任何 GUI 工具都直接。
参数说明:上面命令里的head -30可以根据需要调整,如果你怀疑的问题在低频栈里,加head -100再看;sed 's/at //g'是为了去掉at前缀让计数结果更干净;排序时-rn里的r表示逆序,n表示按数值排序,缺少n的话 23 会排在 9 前面,数字越大越靠前,很容易看错。
4. 必调参数与核心指标:线程状态、锁等待和 CPU 的关系
前面讲了怎么把 Thread Dump 跑起来并聚合出可疑点,但很多人拿到聚合结果后还差最后一步:判断这个可疑线程到底是不是根因。这一步要靠三个核心指标交叉验证——线程状态、锁信息、CPU 占用。三者对不上,定位就是猜;三者能相互印证,结论才值得写进故障报告。
4.1 线程状态分布:先分清五状态再判断问题类型
Java 线程状态在 Thread Dump 里通常表现为五种:
| 状态 | 含义 | 常见场景 | 严重程度 |
|---|---|---|---|
| RUNNABLE | 正在执行(或等待 CPU) | 正常业务处理、GC | 需结合 CPU 判断 |
| BLOCKED | 等待进入 synchronized 块 | 锁竞争 | 高 |
| WAITING | 调用 wait/join/park 无限等待 | 池化线程等待任务 | 低 |
| TIMED_WAITING | 带超时的等待 | sleep、带超时 poll | 视场景 |
| NEW/TERMINATED | 新建/销毁 | 生命周期边界 | 极低 |
WAITING 线程多不代表有问题。线程池里的空闲线程本来就是 WAITING 状态,它们是在等任务队列。很多新手一看到几十个 WAITING 线程就紧张,实际上这是线程池的正常形态。真正需要关注的是 BLOCKED 的绝对数量和占比:如果一个核心线程数为 20 的线程池里,BLOCKED 线程超过 10 个,说明竞争已经导致吞吐量跌了一半以上。
另一种需要警惕的是 RUNNABLE 里的异常情况。RUNNABLE 不代表「没风险」,它只代表「没在等锁」。如果是 CPU 密集型的死循环,线程状态照样是 RUNNABLE,但 CPU 占用会直接飙到核心数×100%。所以 RUNNABLE 状态的判断必须结合操作系统层面的 CPU 数据,这就是第三个指标存在的意义。
4.2 锁信息在堆栈里的真实长相:locked、waiting to lock、parking to wait for
Thread Dump 里的锁信息是定位竞争的全过程凭证,三种关键字对应三种不同的等待语义,先看这段示例:
"Thread-A" #12 prio=5 os_prio=0 cpu=1.2ms elapsed=10.5s tid=0x00007f... nid=0x3a21 RUNNABLE at com.example.cache.LocalCache.reload(LocalCache.java:88) - locked <0x000000076e10a2d8> (a java.lang.Object) "Thread-B" #13 prio=5 os_prio=0 cpu=0.8ms elapsed=10.5s tid=0x00007f... nid=0x3a22 BLOCKED at com.example.cache.LocalCache.reload(LocalCache.java:88) - waiting to lock <0x000000076e10a2d8> (a java.lang.Object)Thread-A 的locked <0x...>表示它持有这把锁,正在锁保护区内执行;Thread-B 的waiting to lock <0x...>表示它在等同一把锁。两个地址完全一致,锁竞争关系一目了然。
第三种关键字是parking to wait for,对应LockSupport.park和java.util.concurrent下的锁实现(如ReentrantLock)。注意它后面的地址指向sun.misc.Unsafe$Park相关的内部对象,直接搜索同一地址时经常搜不到持有者——这是因为它对应的是AbstractQueuedSynchronizer里的exclusiveOwnerThread字段,需要再查看该对象头里的线程引用,或者直接看同一段 dump 中是否有其他线程在unpark操作。
这里有个实战技巧:搜索锁地址时不要只搜一次。先搜完整地址精确匹配,如果找到locked那就结束;如果只找到同类的waiting,说明锁的持有者在当前 dump 快照之前已经释放了锁,你需要看这个锁地址是哪个对象的 Monitor,再去业务代码里找所有加锁点。
4.3 用 CPU 占用反选可疑线程: top、nid 与十六进制转换
线程状态和锁信息能告诉你「谁在等」,但没法告诉你「谁在疯狂消耗 CPU」。要回答后者,得从操作系统层抓数据:
# 查看进程内各线程 CPU 占用,找出 TOP N top -H -p <pid> # 或者使用更稳定的 pidstat pidstat -t -p <pid> 1 5top -H的输出里有个TID列,这个数字是十进制的线程 ID。它和 Thread Dump 里的nid是对应的,但nid是十六进制。因此需要把top -H拿到的十进制 TID 转成十六进制:
# 在 bash 里把十进制转十六进制 printf "%x" <十进制TID>假设top -H显示 TID 为 19251 的线程 CPU 占用为 120%,执行printf "%x" 19251得到4b33,然后去 dump 里搜索nid=0x4b33,你会看到这样的结果:
"Thread-15" #88 daemon prio=5 os_prio=0 cpu=39420.3ms elapsed=402.1s tid=0x... nid=0x4b33 RUNNABLE at java.io.FileInputStream.readBytes(Native Method) at java.io.FileInputStream.read(FileInputStream.java:255)CPU 占用高的线程如果状态是 RUNNABLE 且栈顶在 Native Method,最常见的两种情况是磁盘读写在忙,或者是 JVM 在等待某个外部资源。这时候 Thread Dump 的价值反而没有那么大了,你需要的是strace或者perf去看系统调用。反过来,如果 CPU 高的线程栈顶是 Java 层代码里的while(true)或重复计算,那根因就在 JVM 内部,Thread Dump 已经帮你锁定了问题出在哪个类的哪一行。
注意:
top -H的 CPU 占用是采样值,多核环境下超过 100% 是正常的。一个 8 核机器上线程 CPU 400%,意味着它吃满了 4 个核。抓 dump 前应该先连续采样几次确认高 CPU 线程没有变化,再抓 dump,否则你抓到的可能是那个高 CPU 线程刚好在等 IO 的瞬间,栈显示不出来。
5. Thread Dump 分析避坑指南:五条用血泪换来的排查教训
线程转储分析看着简单,但真实环境里的坑不少。下面五条是我在多次线上故障排查中实际踩过的,每一条都对应着一次「明明抓到了 dump 却得不出结论」的经历。
5.1 现象:抓了 dump,但问题瞬间「消失」
某次排查 CPU 飙高,我 ssh 上去执行jstack -l <pid>,命令跑完后发现负载降下来了,再抓第二份 dump,高 CPU 线程已经不见踪影。
原因:jstack 本身会触发 JVM 的安全点(SafePoint),所有 Java 线程需要在安全点停下才能生成一致的快照。如果你的高 CPU 线程正在做热循环,安全点机制会让它短暂暂停,而这个暂停恰恰打断了它的连续运行状态,导致下一次 dump 里它的栈顶变了。
解决:不要只抓一次。连续抓三份,间隔 5 秒左右,对比 CPU 线程的栈顶是否稳定。如果三次的结果都不一样,说明问题是不稳定的短时现象,要换个时间窗口再抓。同时把top -H采样和抓 dump 的时序尽量贴近,最好在一个脚本里完成:
# 先采样高 CPU 线程,再立刻抓 dump top -H -b -n 1 -p <pid> > /tmp/top_snapshot.txt jstack -l <pid> > /tmp/dump_1.txt sleep 5 top -H -b -n 1 -p <pid> >> /tmp/top_snapshot.txt jstack -l <pid> > /tmp/dump_2.txt5.2 现象:线程池里大量 WAITING 线程,误判为线程泄漏
业务方拿着 dump 过来,说线程池里 200 个线程全部 WAITING,一定是泄漏了。我扫了一眼线程名,发现全是schedule-pool-开头的定时任务线程,而且等待的位置是DelayedWorkQueue.take。
原因:ScheduledThreadPoolExecutor的核心线程在没任务时,本来就停在DelayedWorkQueue.take()上等新任务,这是它的正常休眠姿势,不是泄漏。
解决:看线程状态之前先看线程名和它对应的池类型。catalina-exec-、http-nio-、schedule-pool-各有各的空闲形态,查一下线程池的默认空闲行为再下结论。真正要关注的是线程池里的ThreadPoolExecutor$Worker.run栈帧,以及池实际大小和配置的核心线程数的差异。
5.3 现象:dump 拿到的栈全部指向业务请求,完全看不出来异常
业务高峰期抓的 dump,500 个线程全部 RUNNABLE 在处理正常请求,没有一个 BLOCKED,但接口 RT 就是居高不下。
原因:线程池被打满时,新请求根本没机会进入线程执行,而是在 Tomcat 的Acceptor或线程池的阻塞队列里排队。你抓的是「正在执行」的线程,不是「排队等待」的线程,所以栈里全是正常业务代码。
解决:这种场景要把排查视野从 Thread Dump 扩展到线程池指标。先看jstack里 Tomcat 的 acceptor 线程在做什么,同时去监控平台查线程池的queue.size和activeCount。如果队列堆积明显,说明线程不慢,是线程数量不够或者下游变慢了导致单线程执行时间变长。Thread Dump 在这类问题里只能回答「线程在等谁」,不能直接回答「为什么这么慢」。
5.4 现象:锁竞争找到了,但锁持有者线程在另一个服务的 dump 里
跨服务调用时,本服务的线程阻塞在waiting to lock上,锁地址对应的对象是 RPC 连接池里的某个连接。搜遍整个 dump 也没有locked该地址的线程。
原因:锁对象如果来自第三方库的全局单例(如数据库连接池、HTTP 连接池的内部锁),锁的持有者可能在另一条调用链上,或者锁已经被释放、当前线程正在等待队列中排在更前面的线程释放。对象地址相同不代表持有者在当前 dump 里可见。
解决:对这类内部锁,需要回到锁对象的类型上,去查这个第三方库的源码,看看它的锁粒度是什么、什么操作会长时间持有。常见的数据库连接池、Redis 客户端都有专门的排查文档,比在 dump 里大海捞针有效得多。
5.5 现象:连续抓了五份 dump,仍然定位不到根因
五份 dump 每份间隔 10 秒,线程状态分布几乎完全一致,BLOCKED 的线程位置也没变,但「锁的持有者」始终没出现在 dump 里。
原因:有的锁持有者是 JVM 内部线程(例如 GC 线程、JIT 编译线程),它们不遵循 Java 层的 synchronized 规则,但会影响业务线程的执行。或者,业务锁是通过ReentrantLock实现的,锁状态存在AbstractQueuedSynchronizer的state字段里,dump 里不会打印locked字样,只有parking to wait for。
解决:学会看- locked与- parking to wait for的区别。对于ReentrantLock,需要通过jstack -m(混合模式)抓取包含 native 帧的堆栈,或者用jcmd <pid> Thread.print -l带上更详细的锁信息。还有一个逆向手段:如果锁无法定位,直接kill -3连续打十几次(间隔 1 秒),让 JVM 在安全点上的采样频率变高,有时能把持有者「拍」进某一个 dump 里,这个方法不建议高频使用,会影响线上服务,我在紧急排障时用过,属于没办法的办法。
6. 把可疑线程钉死:双份 Dump 对比法和锁持有者识别技巧
前面讲完了单份 dump 的读法,但单份快照有一个天然缺陷:它只告诉你某个瞬间发生了什么。要确认一个线程是「一直阻塞」还是「刚好路过阻塞」,至少需要两份间隔合适、时序清晰的 dump 做对比。
6.1 双份 Dump 对比法:从「疑似」到「确认」
抓取两份 dump 的最佳间隔是 5~10 秒,太短线程还没完成一次状态切换,太长现场可能已经变化。抓取完成后,把两份 dump 里 BLOCKED 线程的栈顶提取出来做对比:
# 提取每份 dump 中 BLOCKED 线程的线程名和栈顶 for f in dump_*.txt; do echo "=== $f ===" grep -B 2 "java.lang.Thread.State: BLOCKED" "$f" | grep '^"' | awk -F'"' '{print $2}' | sort > "${f}_blocked_threads.txt" done # 对比两份快照的 BLOCKED 线程集合 diff dump_1.txt_blocked_threads.txt dump_2.txt_blocked_threads.txt如果两份 dump 的 BLOCKED 线程集合几乎一致,说明阻塞是持续性的,值得继续往下挖;如果差异很大,说明锁竞争是瞬时的,可能是某个短时大流量触发的,需要结合监控时间窗口再看。真正要拿到的结论是:锁持有者线程在第二份 dump 中是否让出了锁、被阻塞线程是否恢复。如果 10 秒过去了同一个线程还在等同一把锁,那基本可以判定为长时间锁持有。
6.2 从 BLOCKED 到锁持有者的完整定位链路
最后收一个完整例子。某个下午线上接口 RT 从 50ms 涨到 3s,抓了两份 dump,BLOCKED 线程集中在OrderService.submit,等待的锁地址是0x000000076e10a2d8。在 dump 里搜这个地址,找到持有锁的线程栈顶在OrderService.processInventory。继续看这个方法的代码,发现里面调用了外部库存系统的 HTTP 接口,超时时间设成了 30 秒。
根因链路就完整了:库存系统变慢,导致processInventory长时间持有订单锁,所有submit线程全部 BLOCKED。修复方案是缩短超时时间、缩小锁粒度、把远程调用移出锁区块。整个过程只用了两次grep和一次调用链梳理,没有任何高级工具。
说句实在话,Thread Dump 分析这个方向真正值钱的能力不是记住命令,而是养成一种排查习惯:看到 BLOCKED 先找锁地址,找到锁持有者先看它在干什么,再看它在等的外部资源是什么。这套思路走完了,大多数并发问题都不会是黑匣子。做性能排查这几年,我最大的收获就是学会了「相信快照、但不要只信一张快照」。希望帮到你,也祝你的下次故障排查能少加一次班。
本文还有配套的精品资源,点击获取