今天下午做 Code Review,抓到一个非常有意思的典型案例。

组里来了个实习生,我让他写一个简单的后台心跳检测功能,需求很简单:主程序启动后,每隔0.5秒打印一次心跳;主程序业务跑完,心跳自动停止。

逻辑听起来很简单对吧?结果他是这么写的:

public class App {
    public static void main(String[] args) {
        // 1. 启动心跳线程(保安)
        Thread guard = new Thread(() -> {
            while (true) { //埋雷点:永无止境的死循环
                System.out.println("保安正在巡逻...");
                try { Thread.sleep(500); } catch (Exception e) {}
            }
        });
        guard.start();

        // 2. 模拟主业务(员工干活)
        System.out.println("员工正在搬砖...");
        try { Thread.sleep(2000); } catch (Exception e) {}
        System.out.println("员工下班,主线程结束!");
    }
}

现场还原:

他在本地一跑,控制台确实打印了“员工下班”,主方法 main 的括号也都走完了。

但是,控制台依然在疯狂刷屏“保安正在巡逻...”,IDEA 右上角的红色 Stop 按钮依然亮着,JVM 进程根本没退。

实习生挠着头问我:“老大,这不科学啊,main 线程不是结束了吗?为什么程序断不了气?”


JVM 的“死脑筋”:它只认人头

要解释这个现象,别去背那些晦涩的定义,你只需要记住 JVM 关机的一个死规则

在 Java 线程模型(基于 OS 原生线程)中,线程身份分两类:

  1. 用户线程 (User Thread):正规军。main 线程、你手动 new Thread() 出来的,默认都是这种。

  2. 守护线程 (Daemon Thread):后勤兵。比如 GC 垃圾回收线程、JIT 编译线程。

我们来看一下当前的进程状态:

JVM Process
 │
 ├─ Main Thread (User) ────> [Finished]
 │
 └─ Guard Thread (User) ───> [Running] ->罪魁祸首

在上面的代码里,虽然“员工”(main 线程)下班走了,但“保安”(guard 线程)还在 while(true) 里转圈。在 JVM 眼里,这个保安也是个用户线程。只要它还在跑,JVM 就认为“还有重要任务没做完”,绝不关机。

于是,公司里人都走光了,只剩保安对着空气巡逻,电费(CPU/内存)照样烧。


只有一行代码的修正

想解决这个问题,根本不需要设计复杂的 volatile 退出逻辑。Java 专门给这种“打杂”的线程设计了个身份——守护线程

我们只需要告诉 JVM:“这个线程是看大门的,如果楼里的人(用户线程)都走光了,它也就没必要留着了,直接带走。”

错误示范:手动挡的尴尬

很多新手试图用一个 boolean flag 来控制循环退出。虽然能用,但在这种“同生共死”的简单场景下,属于过度设计,而且容易因为并发可见性问题踩坑。

最佳实践:签下“生死契约”

Thread guard = new Thread(() -> {
    while (true) {
        System.out.println("默默守护中...");
        try { Thread.sleep(500); } catch (Exception e) {}
    }
});

//  核心修改:签下“生死契约”
// 注意:这行代码必须在 start() 之前写!否则抛 IllegalThreadStateException
guard.setDaemon(true); 

guard.start();

效果立竿见影

main 线程打印完“下班”结束时,JVM 扫了一眼后台,发现剩下的线程里全是贴着“Daemon”标签的。JVM 二话不说,直接掐断了这些守护线程,IDEA 里的红灯瞬间熄灭,进程完美退出。


进阶:什么时候该用 setDaemon?

这时候可能会有兄弟抬杠:“我在线程里搞个 volatile boolean running 开关,主线程结束前把它改成 false,让它自己跳出循环,不是更优雅?”

这就要看你的业务场景了。这里有个“暴力”与“优雅”的区别。

特性方案 A: setDaemon(true)(官方外挂)方案 B: volatile boolean(手动档)
模式陪葬模式协商模式
JVM行为主线程一死,JVM 直接强行终结守护线程,不留遗言主线程发信号,子线程处理完手头逻辑,体面退出
风险绝对不能做文件写操作!可能写一半被杀,导致文件损坏。代码侵入性强,需要到处检查标记。
适用场景心跳检测、日志上报、JVM 级后台任务。文件下载、转账处理、数据完整性要求高的业务。

回到开头的场景

这个保安只是打印个日志,没啥重要数据。人走茶凉,直接用 setDaemon(true) 带走是最省事的。


面试官如果问这个,怎么怼回去?

Q:Java 里的守护线程和普通线程有什么区别?


避坑指南

写多线程代码时,请养成三个好习惯:

  1. 明确身份:如果是辅助类线程,记得 setDaemon(true)

  2. 遵守时机setDaemon 必须在 start() 之前,否则报 IllegalThreadStateException

  3. 区分场景:关键业务数据,用 flag 标记退出;无关紧要的打杂,用 Daemon

别让你的代码在服务器上当“钉子户”,该退的时候,就得退得干脆利落。

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com