4. 守护线程与用户线程的退出行为
守护线程与用户线程的退出行为
JVM 的退出条件只与用户线程有关:当所有用户线程结束时,JVM 立即退出,此时所有守护线程被强制终止,不会执行完剩余任务,也不会执行 finally 块中的清理逻辑。下面通过一个可运行示例观察这一行为。
实验设计
创建两个线程执行相同的循环任务:
- 用户线程:每 300ms 打印一次进度,共执行 10 次(约 3 秒)。
- 守护线程:每 300ms 打印一次进度,共执行 20 次(约 6 秒),设置为 daemon。
- 主线程:启动两个线程后立即结束。
预期现象:主线程结束后,JVM 等待用户线程执行完 10 次循环后退出;守护线程只执行了约 10 次就被强制终止,其 finally 块不会执行。
完整代码
public class DaemonVsUserDemo {
public static void main(String[] args) {
// 用户线程:执行 10 次循环
Thread userThread = new Thread(() -> {
try {
for (int i = 1; i <= 10; i++) {
System.out.println("[用户线程] 第 " + i + " 次执行");
Thread.sleep(300);
}
System.out.println("[用户线程] 正常结束");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
System.out.println("[用户线程] finally 块执行");
}
}, "user-thread");
// 守护线程:执行 20 次循环
Thread daemonThread = new Thread(() -> {
try {
for (int i = 1; i <= 20; i++) {
System.out.println("[守护线程] 第 " + i + " 次执行");
Thread.sleep(300);
}
System.out.println("[守护线程] 正常结束");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
System.out.println("[守护线程] finally 块执行");
}
}, "daemon-thread");
// 必须在 start() 之前设置 daemon
daemonThread.setDaemon(true);
userThread.start();
daemonThread.start();
System.out.println("[主线程] 结束,JVM 将等待用户线程完成");
}
}
关键约束:
setDaemon(true) 必须在 start() 之前调用,否则抛出 IllegalThreadStateException。守护线程中创建的子线程默认也是守护线程。
退出行为图解
运行结果分析
实际运行输出(示例):
[主线程] 结束,JVM 将等待用户线程完成
[用户线程] 第 1 次执行
[守护线程] 第 1 次执行
[用户线程] 第 2 次执行
[守护线程] 第 2 次执行
...
[用户线程] 第 10 次执行
[用户线程] 正常结束
[用户线程] finally 块执行
(JVM 退出,守护线程停留在第 10 次左右,finally 未打印)
从输出可以确认三个关键行为:
- 主线程结束后,JVM 没有立即退出,而是等待用户线程完成全部 10 次循环。
- 用户线程的 finally 块正常执行,说明用户线程是完整走完生命周期的。
- 守护线程在用户线程结束后被强制终止,其 finally 块没有执行,循环次数停留在 10 次左右(与用户线程同步推进)。
守护线程的适用场景与禁忌
| 适用场景 | 原因 |
|---|---|
| 后台心跳检测 | 主业务结束后心跳无意义,可随时终止 |
| 日志异步刷盘 | JVM 退出时日志已无消费方,强制终止可接受 |
| 缓存过期清理 | 辅助性任务,不承载核心数据一致性 |
| 监控指标采集 | 进程退出后监控数据不再需要 |
禁忌:不要在守护线程中执行文件写入、数据库事务、资源释放等必须完成的操作。守护线程被终止时不会执行 finally,可能导致数据丢失或资源泄漏。需要保证完成的任务必须使用用户线程。
验证要点
- 将
daemonThread.setDaemon(true)改为false,重新运行,观察 JVM 是否等待守护线程完成全部 20 次循环。 - 在守护线程的 finally 块中加入文件写入操作,验证 JVM 退出时该文件是否被创建。
- 用
Thread.currentThread().isDaemon()在守护线程内部打印验证标志位。