点亮城市
72.01 MB · 2026-02-05
今天下午做 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 关机的一个死规则:
在 Java 线程模型(基于 OS 原生线程)中,线程身份分两类:
用户线程 (User Thread):正规军。main 线程、你手动 new Thread() 出来的,默认都是这种。
守护线程 (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 里的红灯瞬间熄灭,进程完美退出。
这时候可能会有兄弟抬杠:“我在线程里搞个 volatile boolean running 开关,主线程结束前把它改成 false,让它自己跳出循环,不是更优雅?”
这就要看你的业务场景了。这里有个“暴力”与“优雅”的区别。
| 特性 | 方案 A: setDaemon(true)(官方外挂) | 方案 B: volatile boolean(手动档) |
|---|---|---|
| 模式 | 陪葬模式 | 协商模式 |
| JVM行为 | 主线程一死,JVM 直接强行终结守护线程,不留遗言。 | 主线程发信号,子线程处理完手头逻辑,体面退出。 |
| 风险 | 绝对不能做文件写操作!可能写一半被杀,导致文件损坏。 | 代码侵入性强,需要到处检查标记。 |
| 适用场景 | 心跳检测、日志上报、JVM 级后台任务。 | 文件下载、转账处理、数据完整性要求高的业务。 |
回到开头的场景:
这个保安只是打印个日志,没啥重要数据。人走茶凉,直接用 setDaemon(true) 带走是最省事的。
Q:Java 里的守护线程和普通线程有什么区别?
写多线程代码时,请养成三个好习惯:
明确身份:如果是辅助类线程,记得 setDaemon(true)。
遵守时机:setDaemon 必须在 start() 之前,否则报 IllegalThreadStateException。
区分场景:关键业务数据,用 flag 标记退出;无关紧要的打杂,用 Daemon。
别让你的代码在服务器上当“钉子户”,该退的时候,就得退得干脆利落。