Skip to content

守护线程

默默在后台服务,用户线程走了,它也得跟着走。

一个故事理解守护线程

想象餐厅里:

  • 服务员(用户线程):接待客人、点餐、上菜。客人走光了,服务员也要下班。
  • 保洁阿姨(守护线程):收拾桌子、打扫卫生。客人走光了,保洁阿姨也要下班。
  • 厨师(用户线程):做菜。客人走光了,厨师也要下班。

但如果你是老板(JVM),你会在所有客人都走了之后,强行让保洁阿姨停下手里的活,而不是等她把桌子擦完。

这就是守护线程的机制:当所有用户线程结束时,JVM 会直接杀死守护线程,不会等它执行完毕

基本概念

用户线程 vs 守护线程

对比项用户线程(User Thread)守护线程(Daemon Thread)
JVM 退出等所有用户线程结束才退出JVM 退出时直接杀死
finally 块保证执行可能不执行
使用场景业务逻辑后台服务

JVM 内置的守护线程

你写的代码里没有创建,但 JVM 自动创建的守护线程:

JVM 启动
  ├── main 用户线程
  ├── GC 守护线程(垃圾回收)
  ├── JIT 守护线程(编译优化)
  ├── Signal Dispatcher 守护线程(信号分发)
  └── Reference Handler 守护线程(引用处理)

基本用法

设置守护线程

java
Thread daemon = new Thread(() -> {
    // 后台任务
    while (true) {
        System.out.println("守护线程运行中");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {}
    }
});

daemon.setDaemon(true);  // 必须在 start() 之前设置
daemon.start();

必须在 start() 之前设置,否则抛 IllegalThreadStateException

java
Thread thread = new Thread(() -> {});

thread.setDaemon(true);  // ✅ 正常
thread.start();          // ✅ 正常

// vs

Thread thread2 = new Thread(() -> {});
thread2.start();
thread2.setDaemon(true);  // ❌ IllegalThreadStateException

演示:守护线程的命运

java
public class DaemonDemo {

    public static void main(String[] args) throws InterruptedException {
        // 用户线程:干 2 秒
        Thread userThread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("用户线程: 第 " + (i + 1) + " 秒");
                sleep(200);
            }
            System.out.println("用户线程结束");
        }, "UserThread");

        // 守护线程:一直跑
        Thread daemonThread = new Thread(() -> {
            int count = 0;
            while (true) {
                System.out.println("守护线程: 第 " + (++count) + " 次");
                sleep(100);
            }
        }, "DaemonThread");

        daemonThread.setDaemon(true);  // 设置为守护线程

        daemonThread.start();
        userThread.start();

        userThread.join();
        System.out.println("主线程结束");
        // 注意:此时 daemonThread 可能还没执行完
        // 但 JVM 退出会直接杀死它
    }

    private static void sleep(int ms) {
        try {
            Thread.sleep(ms);
        } catch (InterruptedException e) {}
    }
}

输出(部分):

守护线程: 第 1 次
用户线程: 第 1 秒
守护线程: 第 2 次
用户线程: 第 2 秒
...
用户线程结束
主线程结束

可以看到,用户线程结束后,守护线程可能还有「第 N 次」没来得及打印,就被 JVM 杀死了。

守护线程适用场景

适合那些断了也不可惜的后台任务:

1. 监控服务

java
public class MonitoringService {

    public static void main(String[] args) {
        // 内存监控
        Thread memoryMonitor = new Thread(() -> {
            Runtime runtime = Runtime.getRuntime();
            while (true) {
                long used = runtime.totalMemory() - runtime.freeMemory();
                System.out.printf("[监控] 内存使用: %.2f MB%n", used / 1024.0 / 1024);
                sleep(5000);
            }
        }, "MemoryMonitor");

        memoryMonitor.setDaemon(true);
        memoryMonitor.start();

        // 业务逻辑
        runBusiness();
    }
}

2. 日志定时刷新

java
// 后台刷新日志缓冲区
Thread logFlusher = new Thread(() -> {
    while (true) {
        flushLogBuffer();  // 刷新日志
        sleep(1000);
    }
}, "LogFlusher");

logFlusher.setDaemon(true);
logFlusher.start();

3. 心跳检测

java
Thread heartbeat = new Thread(() -> {
    while (true) {
        sendHeartbeat();
        sleep(3000);
    }
}, "Heartbeat");

heartbeat.setDaemon(true);
heartbeat.start();

4. 定时任务(简单场景)

java
// 简单的定时清理
Thread cleanup = new Thread(() -> {
    while (true) {
        cleanExpiredCache();
        sleep(3600000);  // 每小时清理一次
    }
}, "CacheCleanup");

cleanup.setDaemon(true);
cleanup.start();

守护线程的坑

坑1:finally 可能不执行

java
// ❌ 危险:守护线程在 finally 执行前就被杀死了
Thread daemon = new Thread(() -> {
    try {
        connection = database.getConnection();
        process();
    } finally {
        connection.close();  // 可能永远不会执行!
    }
});
daemon.setDaemon(true);
daemon.start();

教训:守护线程不适合执行需要保证完整性的操作(数据库事务、文件写入等)。

坑2:线程池的守护线程问题

java
// ❌ 危险:守护线程池的任务可能跑不完
ExecutorService executor = Executors.newCachedThreadPool();
// 默认创建的线程是用户线程,任务能保证执行

// 如果设置为守护线程
((ThreadPoolExecutor) executor).setThreadFactory(r -> {
    Thread t = new Thread(r);
    t.setDaemon(true);  // 设为守护线程
    return t;
});

坑3:不适用于关键后台任务

java
// ❌ 不适合:重要的后台处理
Thread transactionProcessor = new Thread(() -> {
    while (true) {
        processTransaction();  // 重要!
    }
});
transactionProcessor.setDaemon(true);  // ❌ JVM 退出时任务可能没完成

// ✅ 应该用用户线程或 ScheduledExecutorService
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> processTransaction(), 0, 1, TimeUnit.SECONDS);

守护线程 vs ScheduledExecutorService

简单场景用守护线程就够了,但生产环境更推荐 ScheduledExecutorService

对比项守护线程ScheduledExecutorService
任务保证不保证保证执行
异常处理简单完善
灵活性
适用场景非关键后台任务关键定时任务
java
// 生产环境推荐:ScheduledExecutorService
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

// 定时任务
scheduler.scheduleAtFixedRate(() -> {
    System.out.println("定时任务执行");
}, 0, 5, TimeUnit.SECONDS);

// 优雅关闭
scheduler.shutdown();
scheduler.awaitTermination(1, TimeUnit.MINUTES);

正确用法总结

适合守护线程的场景

  • 监控、统计类任务(数据丢了不心疼)
  • 非关键的清理任务
  • 心跳、日志刷新等辅助任务

不适合守护线程的场景

  • 数据库操作、文件写入
  • 需要事务保证的操作
  • 关键业务逻辑

原则:如果任务中断会导致数据丢失或状态不一致,不要用守护线程

总结

  • 守护线程是「默默干活,用户线程走光就下班」的模式
  • 必须 start() 前设置,否则抛异常
  • finally 块可能不执行
  • 适合监控、日志刷新等非关键任务
  • 关键任务用 ScheduledExecutorService 代替守护线程

基于 VitePress 构建