文章目录
- 前言
- [一、先看现象:JVM 到底什么时候退出?](#一、先看现象:JVM 到底什么时候退出?)
-
-
- [1.1 普通线程会阻止 JVM 退出](#1.1 普通线程会阻止 JVM 退出)
- [1.2 守护线程不会阻止 JVM 退出](#1.2 守护线程不会阻止 JVM 退出)
- 二、守护线程为什么存在?
-
- [2.1 守护线程的定位:辅助,而不是主业务](#2.1 守护线程的定位:辅助,而不是主业务)
- [2.2 JVM 自己也在使用守护线程](#2.2 JVM 自己也在使用守护线程)
- 三、开发中如何正确使用守护线程?
-
- [3.1 创建守护线程时,要注意调用时机](#3.1 创建守护线程时,要注意调用时机)
- [3.2 是否使用守护线程,只看任务是否必须完成](#3.2 是否使用守护线程,只看任务是否必须完成)
- [3.3 不要把守护线程当成关闭逻辑](#3.3 不要把守护线程当成关闭逻辑)
-
- 写在文后
🔥 个人主页:铁皮哥(欢迎关注)
📌 作者简介:28届校招生,后端开发/Agent 方向在学
📚 学习内容:Java、Python、计算机视觉、大语言模型、Agent开发
📝 专栏内容:从零开始的Claude Code零代码生活(持续更新中)
✨不只背八股,更想搞懂为什么这样设计
前言
守护线程并不是一种新的线程实现,它本质上仍然是 Java 线程。它和普通线程真正的区别,不在于能不能执行任务,也不在于执行速度快慢,而在于 JVM 判断程序是否应该退出时,会不会把它算进去。
换句话说,普通线程会影响 JVM 的生命周期,而守护线程不会阻止 JVM 退出。
理解这一点之后,很多现象就变得清晰了。
比如为什么 main 方法结束后,程序不一定马上结束;为什么有些后台线程还没执行完,JVM 却可以直接退出;为什么 GC 线程、监控线程、缓存清理线程这类后台任务,通常适合设计成守护线程。
一、先看现象:JVM 到底什么时候退出?
很多初学者以为 Java 程序结束的标志是 main 方法执行完毕,但实际上并非如此。
JVM 判断程序是否结束,并不依赖主线程是否结束,而是依据是否还存在存活的普通线程。只要还有普通线程在运行,JVM 就认为程序还有工作没完成,因此会继续存活。
1.1 普通线程会阻止 JVM 退出
来看一个简单例子:
java
new Thread(() -> {
while (true) {
System.out.println("working...");
}
}).start();
即使 main 方法执行完毕,只要这个线程仍在循环打印日志,JVM 也不会退出。原因在于:
JVM 判断程序结束的标准是存活的普通线程数量,而不是
main线程是否结束。
换句话说,普通线程决定了 JVM 是否继续活着。
1.2 守护线程不会阻止 JVM 退出
如果将同一个线程改为守护线程:
java
Thread thread = new Thread(() -> {
while (true) {
System.out.println("monitor...");
}
});
thread.setDaemon(true);
thread.start();
情况就完全不同了。此时,当所有普通线程结束后,即使守护线程还在运行,JVM 也会直接退出。
由此可以总结一个最核心的结论:
普通线程决定 JVM 是否继续存活,守护线程不会阻止 JVM 退出。
这也是守护线程和普通线程最本质的区别:守护线程的生命周期依附于普通线程,一旦普通线程全部结束,守护线程的存在也随之结束。
二、守护线程为什么存在?
既然守护线程不会阻止 JVM 退出,那 Java 为什么还要设计这种线程?
原因在于,并不是所有线程都代表"程序必须完成的核心任务"。
在一个程序运行过程中,除了真正处理业务的线程之外,还会有很多后台辅助线程。它们不直接完成业务结果,但会在程序运行期间提供支持。守护线程的意义,正是为了表达这种线程的角色定位。
2.1 守护线程的定位:辅助,而不是主业务
守护线程本质上仍然是 Java 线程。
它一样可以执行代码,一样可以参与调度,一样可以在后台运行。它和普通线程的区别,并不是执行能力不同,而是职责不同。
普通线程通常承担的是程序的核心任务。
比如处理用户请求、消费消息、创建订单、写入数据库,这些任务一旦开始执行,通常就应该尽量保证完成。因为它们直接关系到业务结果,如果执行到一半就被中断,很可能会造成数据不一致或者业务异常。
守护线程则更适合承担后台辅助任务。
比如状态监控、缓存清理、心跳检测、资源维护。这些任务在程序运行期间很有价值,但它们本身并不是程序存在的最终目的。程序还在运行时,它们可以持续提供帮助;程序已经结束时,它们也没有必要单独把 JVM 留住。
所以可以这样理解:
普通线程负责把事情做完,守护线程负责在程序活着的时候提供帮助。
这也是"守护"两个字的含义。
它不是程序的主角,而是陪伴程序运行的后台角色。
2.2 JVM 自己也在使用守护线程
其实我们平时写 Java 程序时,即使没有手动创建守护线程,也一直在间接使用它。
最典型的例子就是垃圾回收相关的后台线程。
GC 线程的职责是回收不再使用的对象,帮助 JVM 管理内存。它非常重要,但它的存在是为了服务应用程序运行。只要业务线程还在执行,GC 线程就需要在后台配合工作,避免内存持续膨胀。
但如果所有普通线程都已经结束,说明程序的核心任务已经完成。这个时候,就算 GC 线程还存在,也没有必要为了它继续维持整个 JVM 进程。
这就是守护线程的典型使用场景:
它很重要,但它不决定程序是否继续存在。
类似的后台监控线程、资源清理线程、定时巡检线程,本质上也都符合这个思路。它们适合在程序运行期间默默工作,但不适合成为阻止 JVM 退出的最后一个理由。
因此,守护线程并不是"不重要的线程",而是"不承载最终业务结果的线程"。
理解了这一点,再看守护线程和普通线程的区别,就不会只停留在 setDaemon(true) 这个 API 上,而是能看到背后的设计意图:
JVM 需要区分哪些线程代表核心任务,哪些线程只是为核心任务提供辅助。
三、开发中如何正确使用守护线程?
守护线程并不复杂,真正容易出问题的地方也不是语法,而是使用场景。很多线程相关的问题,往往不是因为不会创建线程,而是把不该放在守护线程里的任务放进去了。
3.1 创建守护线程时,要注意调用时机
创建守护线程的方式很简单,只需要在线程启动之前调用 setDaemon(true):
java
Thread thread = new Thread(() -> {
while (true) {
System.out.println("后台任务运行中...");
}
});
thread.setDaemon(true);
thread.start();
这里有一个细节非常重要:
setDaemon(true) 必须在 start() 之前调用。
如果线程已经启动,再去设置它为守护线程,就会抛出 IllegalThreadStateException。因为线程一旦开始运行,它在 JVM 生命周期管理中的身份就已经确定了,不能中途再从普通线程改成守护线程。
3.2 是否使用守护线程,只看任务是否必须完成
实际开发中,判断一个任务能不能放进守护线程,可以用一个很简单的标准:
如果 JVM 退出时,这个任务没有执行完,会不会造成严重后果?
如果答案是会,那它就不应该使用守护线程。
比如订单处理、消息消费、数据落库、文件导出、支付结果通知,这些任务都属于必须完成的业务逻辑。它们一旦开始执行,通常就需要尽量保证结果完整。如果放在守护线程里,JVM 退出时不会等待它们执行结束,就可能出现任务执行到一半就被终止的情况。
这类任务应该交给普通线程,或者交给具备关闭流程的线程池去处理。程序退出时,也应该通过优雅停机的方式等待任务完成,而不是依赖守护线程自动结束。
反过来,如果一个任务只是辅助性的,程序退出时它没有执行完也可以接受,那它就比较适合使用守护线程。
比如状态监控、心跳检测、缓存清理、后台巡检、临时统计,这些任务存在的意义是服务程序运行本身。程序还活着时,它们可以持续提供帮助;程序结束时,它们跟着一起退出,并不会影响核心业务结果。
所以,守护线程适合做"程序活着时顺手做"的事情,不适合做"程序退出前必须完成"的事情。
3.3 不要把守护线程当成关闭逻辑
还有一个常见误区是:有人会把守护线程理解成"程序退出前专门做收尾工作的线程"。
这个理解是错误的。
守护线程不是关闭钩子,也不是退出前的兜底机制。JVM 退出时,并不会保证守护线程里的代码一定执行完。也就是说,如果你把写日志、刷缓存、关闭连接、保存状态这类关键收尾逻辑放进守护线程,就可能出现代码还没执行完,JVM 就已经结束的情况。
如果确实需要在程序退出前做一些收尾操作,应该使用更明确的关闭机制,例如 shutdown hook、线程池关闭流程,或者框架提供的生命周期回调。
写在文后
期待您的一键三连!如果有什么问题或建议欢迎在评论区交流!