4. 守护线程与用户线程的退出行为
守护线程与用户线程的退出行为
JVM 的退出条件只有一个:所有用户线程(User Thread)都已结束。守护线程(Daemon Thread)不参与这个判断——无论它是否还在运行,只要最后一个用户线程终止,JVM 就会立即退出,守护线程被强制丢弃,finally 块不保证执行。
线程的守护属性由 Thread.setDaemon(boolean) 决定,默认继承父线程(主线程创建的子线程默认是用户线程)。关键约束:setDaemon() 必须在 start() 之前调用,否则抛出 IllegalThreadStateException。下面用一段可运行代码同时验证这两点。
public class DaemonExitDemo {
public static void main(String[] args) throws InterruptedException {
// 1. 用户线程:只跑 1 秒就结束
Thread user = new Thread(() -> {
for (int i = 1; i <= 3; i++) {
System.out.println("[user] tick " + i);
sleep(300);
}
System.out.println("[user] 结束");
}, "user-thread");
// 2. 守护线程:设计成"永不结束",用来观察 JVM 是否等它
Thread daemon = new Thread(() -> {
try {
int n = 0;
while (true) {
System.out.println("[daemon] alive " + (++n));
sleep(300);
}
} finally {
// 若 JVM 等待守护线程,这里一定会打印
System.out.println("[daemon] finally 执行了");
}
}, "daemon-thread");
daemon.setDaemon(true); // 必须在 start() 之前
user.start();
daemon.start();
// 3. 反例:start() 之后再 setDaemon 会抛异常
Thread late = new Thread(() -> {});
late.start();
try {
late.setDaemon(true);
} catch (IllegalThreadStateException e) {
System.out.println("[main] 捕获: " + e.getClass().getSimpleName());
}
user.join(); // 等用户线程结束
System.out.println("[main] 用户线程已结束,main 即将返回");
}
static void sleep(long ms) {
try { Thread.sleep(ms); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}
预期输出(顺序可能因调度略有差异):
[user] tick 1
[daemon] alive 1
[user] tick 2
[daemon] alive 2
[user] tick 3
[daemon] alive 3
[user] 结束
[main] 捕获: IllegalThreadStateException
[main] 用户线程已结束,main 即将返回
(进程退出,daemon 的 finally 未打印)
三个判读要点:
- 进程退出时机:main 返回后 JVM 立即退出,守护线程的
while(true)被强行终止,说明 JVM 不等待守护线程。 - finally 不保证执行:输出中没有
[daemon] finally 执行了,证明守护线程被终止时不会走正常清理路径,因此守护线程不能承担写文件、提交事务等收尾工作。 - setDaemon 时机:
late.start()之后再调用setDaemon(true)抛出IllegalThreadStateException,验证了"必须在 start() 之前"这一约束。
守护线程适合做后台心跳、监控、缓存刷新等"可随时丢弃"的任务;一旦任务涉及资源释放或数据一致性,必须使用用户线程,并通过
join() 或 CountDownLatch 显式等待其结束。验证与边界情况
把 daemon.setDaemon(true) 注释掉再运行一次,会看到守护线程变成用户线程,JVM 不再退出,[daemon] alive 会一直打印下去——这就是"用户线程未结束,JVM 不退出"的直接证据。反过来,把用户线程的循环次数改成 100,守护线程会一直陪跑到用户线程结束,说明守护线程的生命周期上限由用户线程决定。
| 观察项 | 守护线程 | 用户线程 |
|---|---|---|
| 是否阻止 JVM 退出 | 否 | 是 |
| 被终止时 finally 是否执行 | 不保证 | 正常执行 |
| 默认属性 | 继承父线程 | 继承父线程(main 创建默认用户) |
| setDaemon 调用时机 | 必须在 start() 之前,否则抛 IllegalThreadStateException | |
排查"程序不退出"问题时,先用
jstack <pid> 查看是否还有非守护线程存活;若发现业务线程被误设为守护线程导致数据丢失,应改回用户线程并显式 join。