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。守护线程中创建的子线程默认也是守护线程。

退出行为图解

时间 t=0 t≈3s t≈6s 主线程 立即结束 用户线程 10次循环完成 finally 执行 ✓ 守护线程 被强制终止 finally 未执行 ✗ JVM 退出 JVM 退出条件:所有用户线程结束 → 守护线程被强制终止,不等待其完成,不执行 finally 守护线程适用场景:后台监控、心跳检测、日志刷新、缓存清理等可随时中断的辅助任务

运行结果分析

实际运行输出(示例):

[主线程] 结束,JVM 将等待用户线程完成
[用户线程] 第 1 次执行
[守护线程] 第 1 次执行
[用户线程] 第 2 次执行
[守护线程] 第 2 次执行
...
[用户线程] 第 10 次执行
[用户线程] 正常结束
[用户线程] finally 块执行
(JVM 退出,守护线程停留在第 10 次左右,finally 未打印)

从输出可以确认三个关键行为:

  1. 主线程结束后,JVM 没有立即退出,而是等待用户线程完成全部 10 次循环。
  2. 用户线程的 finally 块正常执行,说明用户线程是完整走完生命周期的。
  3. 守护线程在用户线程结束后被强制终止,其 finally 块没有执行,循环次数停留在 10 次左右(与用户线程同步推进)。

守护线程的适用场景与禁忌

适用场景 原因
后台心跳检测 主业务结束后心跳无意义,可随时终止
日志异步刷盘 JVM 退出时日志已无消费方,强制终止可接受
缓存过期清理 辅助性任务,不承载核心数据一致性
监控指标采集 进程退出后监控数据不再需要
禁忌:不要在守护线程中执行文件写入、数据库事务、资源释放等必须完成的操作。守护线程被终止时不会执行 finally,可能导致数据丢失或资源泄漏。需要保证完成的任务必须使用用户线程。

验证要点

  • 将 daemonThread.setDaemon(true) 改为 false,重新运行,观察 JVM 是否等待守护线程完成全部 20 次循环。
  • 在守护线程的 finally 块中加入文件写入操作,验证 JVM 退出时该文件是否被创建。
  • 用 Thread.currentThread().isDaemon() 在守护线程内部打印验证标志位。