jstack可立即捕获因错调用run()导致主线程假死的问题,关键是在卡住瞬间执行快照以保留原始线程状态和干净堆栈,从而精准定位本该start()却直接run()的runnable。

用 jstack 抓住因错调用 run() 导致主线程假死的问题,关键不是等它“挂很久”,而是立刻在刚卡住的瞬间执行快照——因为线程状态还没变、堆栈还干净,能一眼定位到那个本该 start() 却被直接 run() 的 Runnable。
一、先识别“假死”的典型症状
这类问题的表现很“安静”:应用没报错、CPU 不高、日志停在某一步、HTTP 请求超时、UI 无响应。但注意——它和真正的死锁、GC 停顿不同:只有主线程(比如 Tomcat 的 main 线程、Swing 的 Event Dispatch Thread、Spring Boot 的 main)卡住,其他线程照常运行。用 top -H 或 pidstat -t 查看线程 CPU 使用率,大概率发现一个线程 CPU 接近 0%,但持续占用(TIME 高、%CPU 低),这就是“长周期同步执行”在冒泡。
二、秒级抓栈:jstack 要趁“热”执行
一旦怀疑是 run() 直接调用导致阻塞,别重启、别等,立即执行:
-
jstack -l <pid> > jstack-$(date +%s).txt</pid>(加-l可显示锁信息,对排查 synchronized 阻塞很有用) - 如果进程在容器里,先
docker exec -it <container> /bin/sh</container>,再找 Java 进程 PID(ps aux | grep java),最后用jdk自带的jstack(注意 JDK 版本匹配) - 避免用
jstack -F强制 dump(会暂停 JVM,可能干扰现场),除非已确认进程完全无响应
三、从线程堆栈里揪出“错用 run()”的证据
打开 dump 文件,重点扫以下三类线索:
-
主线程栈顶是你的业务类 +
run()方法,但调用链里没有Thread.start()或ThreadPoolExecutor相关帧——例如:"main" #1 prio=5 ... java.lang.Thread.run(Thread.java:834)下面紧跟着com.example.Task.run(Task.java:15),而上面全是yourApp.main(...)或SpringApplication.run(...),这就极可能是手动调了task.run() - 看到
java.util.concurrent.ThreadPoolExecutor$Worker.run或java.lang.Thread.run是正常;但若看到com.yourpkg.YourTask.run直接挂在main线程栈里,且无任何线程创建痕迹,就是铁证 - 留意是否有明显耗时操作:如
Thread.sleep、Object.wait、大循环、IO 阻塞(FileInputStream.read、SocketInputStream.read)出现在run()内部——这些本该交给子线程,却跑在主线程上
四、修复与预防:两行代码的事,但得改习惯
修复本身很简单:
- 把
task.run()改成new Thread(task).start()(适合简单场景) - 更推荐:注入
TaskExecutor或使用CompletableFuture.runAsync(..., executor),让任务走线程池 - 静态检查可加:IDEA 开启 “Runnable#run called directly” 检查;或用 SpotBugs 规则
RV_RETURN_VALUE_IGNORED_NO_SIDE_EFFECT(虽不精准,但配合自定义规则可捕获常见误调)
上线前做一次轻量级线程扫描:写个单元测试,启动后立即 jstack dump,grep "main.*run(",排除可疑调用点。










