前言

最近我踩了一个坑:前端关了,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 进程结束运行。
这里面有几个层次要分清:

  1. 如果应用走到了正常的 JVM 关闭流程,Spring 会尝试优雅关闭容器,执行 @PreDestroy。
  2. 但你手动创建的线程,如果没有在关闭阶段显式 interrupt、shutdown、join、awaitTermination,它们可能来不及收。
  3. 如果 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,只是“让应用停下来”;
而“能不能安全停下来”,取决于你的线程是不是被正确管理。

线程不是开起来就完了,线程最难的部分,往往是怎么把它关干净。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐