4. 守护线程与用户线程行为差异
守护线程与用户线程的本质差异
JVM 判断程序是否结束的唯一依据是:是否还存在存活的用户线程。守护线程(daemon thread)不会阻止 JVM 退出——当所有用户线程终止后,JVM 会立即终止所有守护线程,无论它们是否还在执行任务。这一特性使守护线程天然适合后台服务:心跳检测、日志刷盘、缓存清理、监控上报等。
关键 API 只有两个:thread.setDaemon(true) 必须在 start() 之前调用,否则抛出 IllegalThreadStateException;thread.isDaemon() 用于确认当前状态。守护线程中创建的子线程默认继承守护属性。
验证实验:观察 JVM 是否等待
下面这段代码同时创建用户线程和守护线程,两者都执行 5 秒的循环打印。主线程只休眠 1 秒后结束,观察输出差异。
public class DaemonVsUserDemo {
public static void main(String[] args) throws InterruptedException {
// 用户线程:默认 daemon = false
Thread userThread = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println("[用户线程] 第 " + i + " 秒");
sleep(1000);
}
System.out.println("[用户线程] 任务完成");
}, "user-worker");
// 守护线程:必须在 start() 之前设置
Thread daemonThread = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println("[守护线程] 第 " + i + " 秒");
sleep(1000);
}
System.out.println("[守护线程] 任务完成");
}, "daemon-worker");
daemonThread.setDaemon(true);
userThread.start();
daemonThread.start();
System.out.println("main 线程休眠 1 秒后结束");
Thread.sleep(1000);
System.out.println("main 线程结束");
}
static void sleep(long ms) {
try { Thread.sleep(ms); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}
预期输出(示例):
main 线程休眠 1 秒后结束
[用户线程] 第 1 秒
[守护线程] 第 1 秒
main 线程结束
[用户线程] 第 2 秒
[守护线程] 第 2 秒
[用户线程] 第 3 秒
[守护线程] 第 3 秒
[用户线程] 第 4 秒
[守护线程] 第 4 秒
[用户线程] 第 5 秒
[守护线程] 第 5 秒
[用户线程] 任务完成
[守护线程] 任务完成
main 线程休眠 1 秒后结束
[用户线程] 第 1 秒
[守护线程] 第 1 秒
main 线程结束
[用户线程] 第 2 秒
[守护线程] 第 2 秒
[用户线程] 第 3 秒
[守护线程] 第 3 秒
[用户线程] 第 4 秒
[守护线程] 第 4 秒
[用户线程] 第 5 秒
[守护线程] 第 5 秒
[用户线程] 任务完成
[守护线程] 任务完成
注意:守护线程的“任务完成”是否打印取决于 JVM 退出时机。如果用户线程先结束,守护线程可能来不及打印最后一行就被终止。多次运行可观察到守护线程输出行数在 4~5 之间波动,这正是守护线程“不保证完整执行”的体现。
守护线程的正确使用方式
守护线程适合执行可随时中断、不影响数据一致性的后台任务。以下是一个心跳检测守护线程的典型实现:
public class HeartbeatDaemon {
private static volatile boolean running = true;
public static void main(String[] args) throws InterruptedException {
Thread heartbeat = new Thread(() -> {
while (running) {
System.out.println("[心跳] " + System.currentTimeMillis()
+ " 服务存活");
try { Thread.sleep(2000); }
catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
System.out.println("[心跳] 守护线程退出");
}, "heartbeat-daemon");
heartbeat.setDaemon(true);
heartbeat.start();
// 模拟主业务运行 7 秒
Thread.sleep(7000);
System.out.println("主业务结束,JVM 即将退出");
// 无需手动停止 heartbeat,JVM 退出时自动终止
}
}
使用守护线程的三条准则:
- 守护线程中不要执行写文件、写数据库等有副作用的操作——JVM 可能在任意时刻终止它,导致数据不完整。
- 守护线程中不要持有需要显式释放的资源(如文件句柄、网络连接),因为
finally块不保证执行。 - 如果后台任务必须完整执行,应使用用户线程,并通过
interrupt()或标志位优雅停止。
常见陷阱:setDaemon 调用时机
以下代码会抛出 IllegalThreadStateException:
Thread t = new Thread(() -> System.out.println("run"));
t.start();
t.setDaemon(true); // 抛出 IllegalThreadStateException
正确做法是在 start() 之前调用 setDaemon(true)。另外,守护线程中创建的子线程默认继承守护属性,无需重复设置。
验证要点:运行上述示例时,观察控制台输出中守护线程的最后一行是否出现。如果主线程结束后守护线程立即消失,说明 JVM 没有等待守护线程——这正是本节要验证的核心行为。