守护线程与用户线程
1. 前言
本节内容主要是对守护线程与用户线程进行深入的讲解,具体内容点如下:
- 了解守护线程与用户线程的定义及区别,使我们学习本节内容的基础知识点;
- 了解守护线程的特点,是我们掌握守护线程的第一步;
- 掌握守护线程的创建,是本节内容的重点;
- 通过守护线程与 JVM 的退出实验,更加深入的理解守护线程的地位以及作用,为本节内容次重点;
- 了解守护线程的作用及使用场景,为后续开发过程中提供守护线程创建的知识基础。
2. 守护线程与用户线程的定义及区别
Java 中的线程分为两类,分别为 daemon 线程(守护线程〉和 user 线程(用户线程)。
在 JVM 启动时会调用 main 函数, main 函数所在的线程就是一个用户线程,其实在 JVM 内部同时还启动了好多守护线程,比如垃圾回收线程。
守护线程定义:所谓守护线程,是指在程序运行的时候在后台提供一种通用服务的线程。比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。
因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。
用户线程定义:某种意义上的主要用户线程,只要有用户线程未执行完毕,JVM 虚拟机不会退出。
区别:在本质上,用户线程和守护线程并没有太大区别,唯一的区别就是当最后一个非守护线程结束时,JVM 会正常退出,而不管当前是否有守护线程,也就是说守护线程是否结束并不影响 JVM 的退出。
言外之意,只要有一个用户线程还没结束, 正常情况下 JVM 就不会退出。
3. 守护线程的特点
Java 中的守护线程和 Linux 中的守护进程是有些区别的,Linux 守护进程是系统级别的,当系统退出时,才会终止。
而 Java 中的守护线程是 JVM 级别的,当 JVM 中无任何用户进程时,守护进程销毁,JVM 退出,程序终止。总结来说,Java 守护进程的最主要的特点有:
- 守护线程是运行在程序后台的线程;
- 守护线程创建的线程,依然是守护线程;
- 守护线程不会影响 JVM 的退出,当 JVM 只剩余守护线程时,JVM 进行退出;
- 守护线程在 JVM 退出时,自动销毁。
4. 守护线程的创建
创建方式:将线程转换为守护线程可以通过调用 Thread 对象的 setDaemon (true) 方法来实现。
创建细节:
- thread.setDaemon (true) 必须在 thread.start () 之前设置,否则会跑出一个 llegalThreadStateException 异常。你不能把正在运行的常规线程设置为守护线程;
- 在 Daemon 线程中产生的新线程也是 Daemon 的;
- 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
线程创建代码示例:
public class DemoTest {
public static void main(String[] args) throws InterruptedException {
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
//代码执行逻辑
}
});
threadOne.setDaemon(true); //设置threadOne为守护线程
threadOne. start();
}
}
5. 守护线程与 JVM 的退出实验
为了更好的了解守护线程与 JVM 是否退出的关系,我们首先来设计一个守护线程正在运行,但用户线程执行完毕导致的 JVM 退出的场景。
场景设计:
- 创建 1 个线程,线程名为 threadOne;
- run 方法线程 sleep 1000 毫秒后,进行求和计算,求解 1 + 2 + 3 + … + 100 的值;
- 将线程 threadOne 设置为守护线程;
- 执行代码,最终打印的结果;
- 加入 join 方法,强制让用户线程等待守护线程 threadOne;
- 执行代码,最终打印的结果。
期望结果:
- 未加入 join 方法之前,threadOne 不能执行求和逻辑,无打印输出,因为 main 函数线程执行完毕后,JVM 退出,守护线程也就随之死亡,无打印结果;
- 加入 join 方法后,可以打印求和结果,因为 main 函数线程需要等待 threadOne 线程执行完毕后才继续向下执行,main 函数执行完毕,JVM 退出。
Tips:main 函数就是一个用户线程,main 方法执行时,只有一个用户线程,如果 main 函数执行完毕,用户线程销毁,JVM 退出,此时不会考虑守护线程是否执行完毕,直接退出。
代码实现 - 不加入 join 方法:
public class DemoTest {
public static void main(String[] args){
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum = sum + i;
}
System.out.println("守护线程,最终求和的值为: " + sum);
}
});
threadOne.setDaemon(true); //设置threadOne为守护线程
threadOne. start();
System.out.println("main 函数线程执行完毕, JVM 退出。");
}
}
执行结果验证:
main 函数线程执行完毕, JVM 退出。
从结果上可以看到,JVM 退出了,守护线程还没来得及执行,也就随着 JVM 的退出而消亡了。
代码实现 - 加入 join 方法:
public class DemoTest {
public static void main(String[] args){
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum = sum + i;
}
System.out.println("守护线程,最终求和的值为: " + sum);
}
});
threadOne.setDaemon(true); //设置threadOne为守护线程
threadOne. start();
try {
threadOne.join(); // 加入join 方法
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main 函数线程执行完毕, JVM 退出。");
}
}
执行结果验证:
守护线程,最终求和的值为: 5050
main 函数线程执行完毕, JVM 退出。
从结果来看,守护线程不决定 JVM 的退出,除非强制使用 join 方法使用户线程等待守护线程的执行结果,但是实际的开发过程中,这样的操作是不允许的,因为守护线程,默认就是不需要被用户线程等待的,是服务于用户线程的。
6. 守护线程的作用及使用场景
作用:我们以 GC 垃圾回收线程举例,它就是一个经典的守护线程,当我们的程序中不再有任何运行的 Thread, 程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是 JVM 上仅剩的线程时,垃圾回收线程会自动离开。
它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
应用场景:
- 为其它线程提供服务支持的情况,可选用守护线程;
- 根据开发需求,程序结束时,这个线程必须正常且立刻关闭,就可以作为守护线程来使用;
- 如果一个正在执行某个操作的线程必须要执行完毕后再释放,否则就会出现不良的后果的话,那么这个线程就不能是守护线程,而是用户线程;
- 正常开发过程中,一般心跳监听,垃圾回收,临时数据清理等通用服务会选择守护线程。
7. 小结
掌握用户线程和守护线程的区别点非常重要,在实际的工作开发中,对一些服务型,通用型的线程服务可以根据需要选择守护线程进行执行,这样可以减少 JVM 不可退出的现象,并且可以更好地协调不同种类的线程之间的协作,减少守护线程对高优先级的用户线程的资源争夺,使系统更加的稳定。
本节的重中之重是掌握守护线程的创建以及创建需要注意的事项,了解守护线程与用户线程的区别使我们掌握守护线程的前提。