【个人经验】一次 Spring Boot 停不掉的排查:为什么 IDEA 点了 Stop,线程还活着?
前言
最近我踩了一个坑:前端关了,Spring Boot 也点了停止,结果 Java 进程还活着,端口还占着,第二次启动还会出现各种“串环境”“连错服务”“数据跑偏”的现象。
排查完以后,我最大的感受是:这件事表面像“线程安全”,本质更像“线程生命周期管理”。
很多时候我们以为 Spring Boot 会帮我们把线程都管好,但实际上,它只会管理它知道的那部分。
尤其是项目迁移的时候,你自己 new Thread() 出来的线程、自己建的线程池、自己维护的后台 worker,如果不手动关,Spring 不会替你收尾。
一、Spring Boot 到底帮我们管什么?
Spring Boot 管的是 Spring 容器里的对象生命周期。比如:
- @Component、@Service、@Bean 这些 bean
- 内嵌 Tomcat/Undertow/Jetty
- Spring 管理的任务执行器、调度器
- @PreDestroy、DisposableBean、SmartLifecycle 这些关闭回调
也就是说,Spring Boot 擅长管理“容器内的资源”。
但如果写了这些:
- new Thread(...)
- Executors.newSingleThreadExecutor(...)
- Executors.newFixedThreadPool(...)
- Timer
- 非 Spring 管理的 ScheduledExecutorService
那这些线程的生死,默认就是你自己负责。
二、为什么 IDEA 点了 Stop,线程还是退不干净?
IDEA 的 Stop,不等于“帮你优雅关闭业务线程”。
它更接近于:让当前这个 JVM 进程结束运行。
这里面有几个层次要分清:
- 如果应用走到了正常的 JVM 关闭流程,Spring 会尝试优雅关闭容器,执行 @PreDestroy。
- 但你手动创建的线程,如果没有在关闭阶段显式 interrupt、shutdown、join、awaitTermination,它们可能来不及收。
- 如果 IDE 最终是更强硬地把进程结束掉,那就更谈不上“优雅收尾”了。
所以“点了 Stop 还没退干净”,不一定是 Spring Boot 不行,而是你的线程根本不在 Spring 的托管范围里。
三、jcmd -l 能看什么?它是干嘛的?
很多人第一次用 jcmd -l,以为它能直接看线程。其实不是。
jcmd -l 的作用是:
- 列出本机当前所有 Java 进程
- 显示 PID
- 显示主类名或者 JAR 路径
它解决的是“我现在到底有几个 JVM 活着”这个问题。
常用命令是:
jcmd -l你会看到类似:
3392 xxx/mainbot.jar 25312 org.jetbrains.jps.cmdline.Launcher
这就能帮你判断:
- 哪个是你的 Spring Boot
- 哪个是 IDEA 编译器进程
- 有没有上一次残留的 Java 进程
如果你想进一步看线程,不是只用 jcmd -l,而是继续用:
jcmd <pid> Thread.print再进一步还可以看:
jcmd <pid> VM.system_properties jcmd <pid> GC.heap_info
所以一句话总结:
- jcmd -l:看“有哪些 JVM”
- jcmd <pid> Thread.print:看“这个 JVM 里有哪些线程”
四、Java 线程基础
这次排查也让我重新把 Java 线程基础想了一遍。
1. 进程和线程不是一回事
- 一个 Java 应用启动后,先有一个 JVM 进程
- 这个 JVM 进程里面可以有很多线程
很多“线程没退”的现象,最后体现成的是“进程还活着”。
2. 用户线程和守护线程不一样
- 用户线程:只要它还活着,JVM 就不会自然退出
- 守护线程(daemon):JVM 退出时不会等它
守护线程不阻止 JVM 退出,但也意味着它可能被直接丢下,不一定有机会把数据写完。
3. interrupt() 不是强杀
interrupt() 的本质是“发一个中断信号”,不是“立即杀死线程”。
线程要自己响应中断,才能优雅结束。
4. 线程池必须自己关
如果用了 ExecutorService,一定要记得:
- shutdown()
- shutdownNow()
- awaitTermination(...)
否则线程池可能一直挂着。
5. 线程安全不只是加锁
提“线程安全”,第一反应就是:
- synchronized
- Lock
- volatile
- AtomicInteger
这些当然重要,但这次我更深的感受是:
线程安全不仅是共享数据安全,还是生命周期安全。
能不能正确启动线程、停止线程、回收线程,同样是“安全”的一部分。
五、这次排查给我的几个感悟
第一,Spring Boot 不是万能托管。
它能管 bean,能管容器,但管不了你所有手搓线程。
第二,new Thread() 很爽,但后患也很大。
启动简单,关闭麻烦,排查更麻烦。
第三,能启动不代表能关闭。
很多程序“跑起来没问题”,但真正难的是“停下来还干净”。
第四,排查这类问题一定先看“进程”,再看“线程”。
不要一上来就怀疑业务逻辑,先用 jcmd -l 确认是不是有旧 JVM 残留。
第五,日志和线程命名特别重要。
如果你的线程名都是默认的 pool-1-thread-1,排查起来会非常痛苦。线程起个有语义的名字,值回票价。
如果是 Spring Boot 项目,我现在更倾向于这样做:
- 能交给 Spring 管的线程池,就不要自己 new
- 自建线程池尽量封装成 bean
- 关闭时统一走 @PreDestroy
- 对线程池显式 shutdown + awaitTermination
- 对手动线程显式 interrupt + join
- 每个重要线程都起名字
- 出现“停不掉”先跑 jcmd -l
总结
很多所谓的“线程安全问题”,其实不是数据竞争,而是生命周期失控。
Spring Boot 帮我们做了很多,但它不是操作系统,也不是线程托管器。只要线程不是它管的,我们就必须自己负责它的出生、运行和死亡。
IDEA 点 Stop,只是“让应用停下来”;
而“能不能安全停下来”,取决于你的线程是不是被正确管理。
线程不是开起来就完了,线程最难的部分,往往是怎么把它关干净。
更多推荐

所有评论(0)