什么是守护线程?
守护线程(Daemon Thread)是Java中的一种特殊线程,那么相对于普通线程它有什么特别之处呢?
在了解守护线程之前,我们先来思考一个问题:JVM在什么情况下会正常退出?答案是:当Java虚拟机(JVM)中只剩下守护线程在运行时,JVM就会自动退出。
上面这句话出自JDK官方文档,原话翻译过来是这样:当JVM中没有正在运行的非守护线程(用户线程)时,JVM进程就会自动退出。这句话可能有些难以理解,但通过下面的代码示例,你就能更清楚地了解它的含义。
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
// 创建一个用户线程
Thread userThread = new Thread(() -> {
while (true) {
System.out.println("User thread is running...");
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
userThread.start();
Thread.sleep(1000);
Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("ShutdownHook is running...")));
System.out.println("The main thread is about to finish execution");
}
}
//输出:
User thread is running...
The main thread is about to finish execution
User thread is running...
User thread is running...
User thread is running...
User thread is running...
User thread is running...
..............
我们创建了一个用户线程,它的逻辑是一个无限循环。运行这段代码后,你可以猜一猜:当主线程结束后,JVM进程会自动退出吗?
答案是不会退出。因为有一个非守护线程(用户线程)一直在后台运行,JVM无法自动退出,ShutdownHook线程的相关逻辑也不会执行。相反,用户线程会继续运行,每隔2秒输出一次 “User thread is running…”。
那么,如果在这种情况下运行的线程是守护线程,又会发生什么呢?
可以通过调用 Thread.setDaemon(true) 方法将一个线程设置为守护线程,代码如下:
public class Demo2 {
public static void main(String[] args) throws InterruptedException {
// 创建一个守护线程
Thread thread = new Thread(() -> {
while (true) {
System.out.println("Daemon thread is running...");
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.setDaemon(true);
thread.start();
Thread.sleep(1000);
Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("ShutdownHook is running...")));
System.out.println("The main thread is about to finish execution");
}
}
//输出:
Daemon thread is running...
The main thread is about to finish execution
ShutdownHook is running...
在这种情况下,当主线程退出时,JVM 也会退出,并且所有守护线程都会被终止。即使守护线程中有死循环,也不会阻止 JVM 的退出。
守护线程的作用和使用场景
角色
在Java中,引入守护线程的主要目的是提供一种用于执行后台任务的机制。守护线程在程序的生命周期中起辅助作用,为其他线程提供支持和服务。
一个典型的例子是GC(垃圾回收器)线程,它就是一个典型的守护线程。它的主要作用是实时监控 JVM 的内存使用情况,并回收那些不再使用的对象,以释放内存空间。
当所有用户线程都结束运行时,程序的生命周期也就结束了,程序不会再产生新的垃圾,此时不再需要执行垃圾回收操作。因此,GC 线程也就没有继续运行的必要。因此,当垃圾回收线程是JVM中唯一剩下的线程时,JVM会自动退出。
应用场景
-
后台任务:守护线程通常用于执行与主线程无关的后台任务,这些任务可以在程序后台静默运行。例如,日志记录、定时任务、系统监控等操作,都可以使用守护线程来完成。
-
垃圾回收:垃圾回收是Java虚拟机的重要功能,它负责回收不再使用的对象并释放内存。垃圾回收器是一个守护线程,在程序运行时自动执行,清理不再需要的对象。
-
资源管理:守护线程可以用于资源管理,例如管理数据库连接池的线程池。它可以监控空闲的数据库连接,如果连接空闲时间过长,守护线程可以自动关闭连接以避免资源浪费。
-
后台服务:在服务器端应用中,守护线程常用于提供持续的服务。例如,在Web服务器中,守护线程可以监听客户端请求,当所有客户端连接断开时,守护线程会自动关闭服务器。
守护线程的注意事项
需要注意的是,守护线程的终止是不可控的,也就是说,守护线程的生命周期由 JVM 决定,而不是程序本身。当程序中只剩下守护线程时,它们会随着主线程的结束而自动终止。因此守护线程在终止时可能并没有机会去“干净地”完成正在进行的任务。
因此,在使用守护线程时,应确保它们执行的任务是可中断的或可恢复的,避免影响程序的整体逻辑。
可中断的任务意味着线程可以在被中断时安全地停止执行。例如,守护线程在进行一个长期任务时,应定期检查线程的中断状态,并在需要时退出或释放资源,这样可以防止数据损坏或丢失,例如如果一个守护线程正在执行一个关键的任务(如写入文件、数据库操作等),而此时 JVM 终止了它,可能导致数据的不完整或丢失。
public class SafeDaemonThreadExample {
public static void main(String[] args) {
// 创建一个守护线程
Thread daemonThread = new Thread(() -> {
try {
while (true) {
System.out.println("守护线程正在运行...");
// 模拟执行某个长期任务
Thread.sleep(1000); // 任务期间可能会被中断
}
} catch (InterruptedException e) {
System.out.println("守护线程被中断,安全退出...");
}
});
// 设置为守护线程
daemonThread.setDaemon(true);
daemonThread.start();
// 主线程等待一段时间后退出
try {
Thread.sleep(3000);
// 主线程结束前主动中断守护线程
daemonThread.interrupt();
System.out.println("主线程结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//输出:
守护线程正在运行...
守护线程正在运行...
守护线程正在运行...
主线程结束
守护线程被中断,安全退出...
可恢复的任务意味着当守护线程执行某些任务时,这些任务应该能够在中断或意外停止后恢复。也就是说,即使守护线程突然停止,这个任务所带来的影响应该是可以被补救的。例如,如果守护线程正在写入一个文件或者数据库,它应该能够保证:
- 不会因为突然停止而留下不完整或损坏的数据。
- 程序可以在稍后恢复此任务并继续进行。
不可恢复的守护线程
public class NonRecoverableDaemonExample {
public static void main(String[] args) {
// 创建一个守护线程
Thread daemonThread = new Thread(() -> {
while (true) {
try {
// 模拟数据库更新
System.out.println("守护线程正在执行数据库更新操作...");
// 这里发生了数据更新操作
// 没有事务或恢复机制
Thread.sleep(1000); // 模拟任务执行时间
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 设置线程为守护线程
daemonThread.setDaemon(true);
daemonThread.start();
// 主线程等待一段时间后退出
try {
Thread.sleep(3000);
System.out.println("主线程结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在这个例子中:
- 守护线程负责执行数据库的更新操作。
- 守护线程在执行过程中没有使用任何事务(transaction)或恢复(recovery)机制。
- 如果程序在更新操作的中间突然停止(例如,JVM 退出了,因为主线程结束),数据库的状态可能会变得不一致。比如,部分数据更新了,而其他数据没有更新,导致数据库中的数据处于一个不完整或损坏的状态。
可恢复的守护线程
现在,我们来看看如何设计一个可恢复的任务。这里我们使用事务(Transaction)来确保数据的一致性和完整性:
public class RecoverableDaemonExample {
public static void main(String[] args) {
// 创建一个守护线程
Thread daemonThread = new Thread(() -> {
while (true) {
try {
// 模拟数据库更新操作
System.out.println("守护线程正在执行数据库更新操作...");
// 开始一个数据库事务
startTransaction();
// 执行更新操作
performDatabaseUpdate();
// 提交事务
commitTransaction();
Thread.sleep(1000); // 模拟任务执行时间
} catch (InterruptedException e) {
System.out.println("守护线程被中断,安全退出...");
} catch (Exception ex) {
// 回滚事务,确保数据不损坏
rollbackTransaction();
System.out.println("发生异常,已回滚事务以确保数据一致性");
}
}
});
// 设置线程为守护线程
daemonThread.setDaemon(true);
daemonThread.start();
// 主线程等待一段时间后退出
try {
Thread.sleep(3000);
System.out.println("主线程结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 模拟数据库操作方法
private static void startTransaction() {
System.out.println("开始事务");
}
private static void performDatabaseUpdate() {
System.out.println("执行数据库更新操作");
}
private static void commitTransaction() {
System.out.println("提交事务");
}
private static void rollbackTransaction() {
System.out.println("回滚事务");
}
}
修改后的守护线程在进行数据库更新时,使用了事务机制,如果在执行过程中发生异常或中断,事务会回滚(rollbackTransaction),确保所有的操作要么全部成功,要么全部失败。这样即使守护线程在中间停止,也不会造成数据损坏或丢失,保证了数据的一致性和完整性。