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 显式等待其结束。
t=0 t≈0.9s t≈1.0s t user-thread(用户线程) tick1 → tick2 → tick3 → 结束 TERMINATED daemon-thread(守护线程) alive1 → alive2 → alive3 → …(while true) 被强制终止 main(用户线程) start → join(user) → 返回 返回 JVM 退出 退出条件:所有用户线程结束 → JVM 立即退出,不等待守护线程 守护线程 finally 不保证执行;setDaemon(true) 必须在 start() 之前

验证与边界情况

把 daemon.setDaemon(true) 注释掉再运行一次,会看到守护线程变成用户线程,JVM 不再退出,[daemon] alive 会一直打印下去——这就是"用户线程未结束,JVM 不退出"的直接证据。反过来,把用户线程的循环次数改成 100,守护线程会一直陪跑到用户线程结束,说明守护线程的生命周期上限由用户线程决定。

观察项守护线程用户线程
是否阻止 JVM 退出否是
被终止时 finally 是否执行不保证正常执行
默认属性继承父线程继承父线程(main 创建默认用户)
setDaemon 调用时机必须在 start() 之前,否则抛 IllegalThreadStateException
排查"程序不退出"问题时,先用 jstack <pid> 查看是否还有非守护线程存活;若发现业务线程被误设为守护线程导致数据丢失,应改回用户线程并显式 join。