JVM中的守护线程示例详解

前言

在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程)

用个比较通俗的比如,任何一个守护线程都是整个JVM中所有非守护线程的保姆:

只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。

Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者。

在之前的《详解JVM如何处理异常》提到了守护线程,当时没有详细解释,所以打算放到今天来解释说明一下JVM守护线程的内容。

特点

  • 通常由JVM启动
  • 运行在后台处理任务,比如垃圾回收等
  • 用户启动线程执行结束或者JVM结束时,会等待所有的非守护线程执行结束,但是不会因为守护线程的存在而影响关闭。

判断线程是否为守护线程

判断一个线程是否为守护线程,主要依据如下的内容

/* Whether or not the thread is a daemon thread. */
private boolean daemon = false;

/**
* Tests if this thread is a daemon thread.
*
* @return <code>true</code> if this thread is a daemon thread;
*  <code>false</code> otherwise.
* @see #setDaemon(boolean)
*/
public final boolean isDaemon() {
 return daemon;
}

下面我们进行一些简单的代码,验证一些关于守护线程的特性和一些猜测。

辅助方法

打印线程信息的方法,输出线程的组,是否为守护线程以及对应的优先级。

private static void dumpAllThreadsInfo() {
 Set<Thread> threadSet = Thread.getAllStackTraces().keySet();
 for(Thread thread: threadSet) {
 System.out.println("dumpAllThreadsInfo thread.name=" + thread.getName()
  + ";group=" + thread.getThreadGroup()
  + ";isDaemon=" + thread.isDaemon()
  + ";priority=" + thread.getPriority());
 }
}

线程睡眠的方法

private static void makeThreadSleep(long durationInMillSeconds) {
 try {
 Thread.sleep(durationInMillSeconds);
 } catch (InterruptedException e) {
 e.printStackTrace();
 }

}

验证普通的(非守护线程)线程会影响进程(JVM)退出

private static void testNormalThread() {
 long startTime = System.currentTimeMillis();
 new Thread("NormalThread") {
 @Override
 public void run() {
  super.run();
  //保持睡眠,确保在执行dumpAllThreadsInfo时,该线程不会因为退出导致dumpAllThreadsInfo无法打印信息。
  makeThreadSleep(10 * 1000);
  System.out.println("startNormalThread normalThread.time cost=" + (System.currentTimeMillis() - startTime));
 }
 }.start();
 //主线程暂定3秒,确保子线程都启动完成
 makeThreadSleep(3 * 1000);
 dumpAllThreadsInfo();
 System.out.println("MainThread.time cost = " + (System.currentTimeMillis() - startTime));
}

获取输出日志

dumpAllThreadsInfo thread.name=Signal Dispatcher;group=java.lang.ThreadGroup[name=system,maxpri=10];isDaemon=true;priority=9
dumpAllThreadsInfo thread.name=Attach Listener;group=java.lang.ThreadGroup[name=system,maxpri=10];isDaemon=true;priority=9
dumpAllThreadsInfo thread.name=Monitor Ctrl-Break;group=java.lang.ThreadGroup[name=main,maxpri=10];isDaemon=true;priority=5
dumpAllThreadsInfo thread.name=Reference Handler;group=java.lang.ThreadGroup[name=system,maxpri=10];isDaemon=true;priority=10
dumpAllThreadsInfo thread.name=main;group=java.lang.ThreadGroup[name=main,maxpri=10];isDaemon=false;priority=5
dumpAllThreadsInfo thread.name=NormalThread;group=java.lang.ThreadGroup[name=main,maxpri=10];isDaemon=false;priority=5
dumpAllThreadsInfo thread.name=Finalizer;group=java.lang.ThreadGroup[name=system,maxpri=10];isDaemon=true;priority=8
MainThread.time cost = 3009
startNormalThread normalThread.time cost=10003
Process finished with exit code 0 结束进程

我们根据上面的日志,我们可以发现

  • startNormalThread normalThread.time cost=10003代表着子线程执行结束,先于后面的进程结束执行。
  • Process finished with exit code 0 代表 结束进程

以上日志可以验证进程是在我们启动的子线程结束之后才退出的。

验证JVM不等待守护线程就会结束

其实上面的例子也可以验证JVM不等待JVM启动的守护线程(Reference Handler,Signal Dispatcher等)执行结束就退出。

这里我们再次用一段代码验证一下JVM不等待用户启动的守护线程结束就退出的事实。

private static void testDaemonThread() {
 long startTime = System.currentTimeMillis();
 Thread daemonThreadSetByUser = new Thread("daemonThreadSetByUser") {
  @Override
  public void run() {
   makeThreadSleep(10 * 1000);
   super.run();
   System.out.println("daemonThreadSetByUser.time cost=" + (System.currentTimeMillis() - startTime));
  }
 };
 daemonThreadSetByUser.setDaemon(true);
 daemonThreadSetByUser.start();
 //主线程暂定3秒,确保子线程都启动完成
 makeThreadSleep(3 * 1000);
 dumpAllThreadsInfo();
 System.out.println("MainThread.time cost = " + (System.currentTimeMillis() - startTime));
}

上面的结果得到的输出日志为

dumpAllThreadsInfo thread.name=Signal Dispatcher;group=java.lang.ThreadGroup[name=system,maxpri=10];isDaemon=true;priority=9
dumpAllThreadsInfo thread.name=Attach Listener;group=java.lang.ThreadGroup[name=system,maxpri=10];isDaemon=true;priority=9
dumpAllThreadsInfo thread.name=Monitor Ctrl-Break;group=java.lang.ThreadGroup[name=main,maxpri=10];isDaemon=true;priority=5
dumpAllThreadsInfo thread.name=Reference Handler;group=java.lang.ThreadGroup[name=system,maxpri=10];isDaemon=true;priority=10
dumpAllThreadsInfo thread.name=main;group=java.lang.ThreadGroup[name=main,maxpri=10];isDaemon=false;priority=5
dumpAllThreadsInfo thread.name=daemonThreadSetByUser;group=java.lang.ThreadGroup[name=main,maxpri=10];isDaemon=true;priority=5
dumpAllThreadsInfo thread.name=Finalizer;group=java.lang.ThreadGroup[name=system,maxpri=10];isDaemon=true;priority=8
MainThread.time cost = 3006

Process finished with exit code 0

我们可以看到,上面的日志没有类似daemonThreadSetByUser.time cost=的信息。可以确定JVM没有等待守护线程结束就退出了。

注意:

  • 新的线程是否初始为守护线程,取决于启动该线程的线程是否为守护线程。
  • 守护线程默认启动的线程为守护线程,非守护线程启动的线程默认为非守护线程。
  • 主线程(非守护线程)启用一个守护线程,需要调用Thread.setDaemon来设置启动线程为守护线程。

关于Priority与守护线程的关系

有一种传言为守护线程的优先级要低,然而事实是

  • 优先级与是否为守护线程没有必然的联系
  • 新的线程的优先级与创建该线程的线程优先级一致。
  • 但是建议将守护线程的优先级降低一些。

感兴趣的可以自己验证一下(其实上面的代码已经有验证了)

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • 详解Java虚拟机(JVM)运行时

    JVM(Java虚拟机)是一个抽象的计算模型.就如同一台真实的机器,它有自己的指令集和执行引擎,可以在运行时操控内存区域.目的是为构建在其上运行的应用程序提供一个运行环境.JVM可以解读指令代码并与底层进行交互:包括操作系统平台和执行指令并管理资源的硬件体系结构.本文主要介绍Java虚拟机(JVM)运行时详解. 我们知道的JVM内存区域有:堆和栈,这是一种泛的分法,也是按运行时区域的一种分法,堆是所有线程共享的一块区域,而栈是线程隔离的,每个线程互不共享. 线程不共享区域 每个线程的数据区域包括

  • JVM:晚期(运行期)优化的深入理解

    晚期(运行期)优化 在部分的商用虚拟机中,Java程序最初是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为热点代码.为了提高热点代码的执行效率,在运行时,虚拟机会将这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器 本章提到的编译器.即时编译器都是指HotSpot虚拟机内的即时编译器,虚拟机也是特质HotSpot虚拟机 1.HotSpot虚拟机内的即时编译器 1).解释器与编译器 当程序需要迅速启动和执行的

  • java JVM原理与常识知识点

    JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的.Java虚拟机包括一套字节码指令集.一组寄存器.一个栈.一个垃圾回收堆和一个存储方法域. JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行.JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行. 1.

  • 详解JVM 运行时内存使用情况监控

    java 语言, 开发者不能直接控制程序运行内存, 对象的创建都是由类加载器一步步解析, 执行与生成与内存区域中的; 并且jvm有自己的垃圾回收器对内存区域管理, 回收; 但是我们已经可以通过一些工具来在程序运行时查看对应的jvm内存使用情况, 帮助更好的分析与优化我们的代码; 注: 查看系统里java进程信息 // 查看当前机器上所有运行的java进程名称与pid(进程编号) jps -l // 显示指定的jvm进程所有的属性设置和配置参数 jinfo pid 1 . jmap : 内存占用情

  • JVM:早期(编译期)优化的深入理解

    早期(编译期)优化 JVM的编译器可以分为三个编译器: 前端编译器:把*.java转变为*.class的过程.如Sun的Javac.Eclipse JDT中的增量式编译器(ECJ) JIT编译器:把字节码转变为机器码的过程,如HotSpot VM的C1.C2编译器 AOT编译器:静态提前编译器,直接将*.java文件编译本地机器代码的过程 本章的后续文字里,"编译期"和"编译器"都仅限于第一类编译过程 1.Javac编译器 Javac编译器本身就是一个由Java语言

  • JVM处理未捕获异常的方法详解

    前言 继之前的文章详解JVM如何处理异常,今天再次发布一篇比较关联的文章,如题目可知,今天聊一聊在JVM中线程遇到未捕获异常的问题,其中涉及到线程如何处理未捕获异常和一些内容介绍. 什么是未捕获异常 未捕获异常指的是我们在方法体中没有使用try-catch捕获的异常,比如下面的例子 private static void testUncaughtException(String arg) { try { System.out.println(1 / arg.length()); } catch

  • JVM进阶教程之字段访问优化浅析

    前言 在实际中,Java程序中的对象或许 本身就是逃逸 的,或许因为 方法内联不够彻底 而被即时编译器 当成是逃逸 的,这两种情况都将 导致即时编译器 无法进行标量替换 ,这时,针对对象字段访问的优化显得更为重要. static int bar(Foo o, int x) { o.a = x; return o.a; } 对象o是传入参数, 不属于逃逸分析的范围 (JVM中的逃逸分析针对的是 新建对象 ) 该方法会将所传入的int型参数x的值存储至实例字段Foo.a中,然后再读取并返回同一字段的

  • JVM如何处理异常深入详解

    前言 无论你是使用何种编程语言,在日常的开发过程中,都会不可避免的要处理异常.今天本文将尝试讲解一些JVM如何处理异常问题,希望能够讲清楚这个内部的机制,如果对大家有所启发和帮助,则甚好. 当异常不仅仅是异常 我们在标题中提到了异常,然而这里指的异常并不是单纯的Exception,而是更为宽泛的Throwable.只是我们工作中习以为常的将它们(错误地)这样称谓. 关于Exception和Throwable的关系简单描述一下 Exception属于Throwable的子类,Throwable的另

  • 浅析JVM逃逸的原理及分析

    我们都知道Java中的对象默认都是分配到堆上,在调用栈中,只保存了对象的指针.当对象不再使用后,需要依靠GC来遍历引用树并回收内存.如果堆中对象数量太多,回收对象还有整理内存,都会会带来时间上的消耗,GC表示压力很大,然后影响性能.所以,在我们日常开发中,内存,时间都是相当的宝贵,该如何优化堆栈开销,是一个比较重要的问题. 在这里,我以逃逸分析角度聊聊JVM优化的那些事儿. 为什么"逃逸" 在计算机语言编译器优化原理中,逃逸分析是指分析指针动态范围的方法,它同编译器优化原理的指针分析和

  • JVM指令的使用深入详解

    一.未归类系列A 此系列暂未归类. 指令码    助记符                            说明 0x00         nop                                什么都不做 0x01        aconst_null                   将null推送至栈顶 二.const系列 该系列命令主要负责把简单的数值类型送到栈顶.该系列命令不带参数.注意只把简单的数值类型送到栈顶时,才使用如下的命令. 比如对应int型才该方式只能把

随机推荐