- Java并发编程之美
- 翟陆续 薛宾田
- 1426字
- 2020-08-27 23:32:55
1.10 守护线程与用户线程
Java中的线程分为两类,分别为daemon线程(守护线程)和user线程(用户线程)。在JVM启动时会调用main函数,main函数所在的线程就是一个用户线程,其实在JVM内部同时还启动了好多守护线程,比如垃圾回收线程。那么守护线程和用户线程有什么区别呢?区别之一是当最后一个非守护线程结束时,JVM会正常退出,而不管当前是否有守护线程,也就是说守护线程是否结束并不影响JVM的退出。言外之意,只要有一个用户线程还没结束,正常情况下JVM就不会退出。
那么在Java中如何创建一个守护线程?代码如下。
public static void main(String[] args) { Thread daemonThread = new Thread(new Runnable() { public void run() { } }); //设置为守护线程 daemonThread.setDaemon(true); daemonThread.start(); }
只需要设置线程的daemon参数为true即可。
下面通过例子来理解用户线程与守护线程的区别。首先看下面的代码。
public static void main(String[] args) { Thread thread = new Thread(new Runnable() { public void run() { for(; ; ){} } }); //启动子线程 thread.start();
System.out.print("main thread is over"); }
输出结果如下。
如上代码在main线程中创建了一个thread线程,在thread线程里面是一个无限循环。从运行代码的结果看,main线程已经运行结束了,那么JVM进程已经退出了吗?在IDE的输出结果右上侧的红色方块说明,JVM进程并没有退出。另外,在mac上执行jps会输出如下结果。
这个结果说明了当父线程结束后,子线程还是可以继续存在的,也就是子线程的生命周期并不受父线程的影响。这也说明了在用户线程还存在的情况下JVM进程并不会终止。那么我们把上面的thread线程设置为守护线程后,再来运行看看会有什么结果:
//设置为守护线程 thread.setDaemon(true); //启动子线程 thread.start();
输出结果如下。
在启动线程前将线程设置为守护线程,执行后的输出结果显示,JVM进程已经终止了,执行ps -eaf |grep java也看不到JVM进程了。在这个例子中,main函数是唯一的用户线程,thread线程是守护线程,当main线程运行结束后,JVM发现当前已经没有用户线程了,就会终止JVM进程。由于这里的守护线程执行的任务是一个死循环,这也说明了如果当前进程中不存在用户线程,但是还存在正在执行任务的守护线程,则JVM不等守护线程运行完毕就会结束JVM进程。
main线程运行结束后,JVM会自动启动一个叫作DestroyJavaVM的线程,该线程会等待所有用户线程结束后终止JVM进程。下面通过简单的JVM代码来证明这个结论。
翻看JVM的代码,能够发现,最终会调用到JavaMain这个C函数。
int JNICALL JavaMain(void * _args) { ... //执行Java中的main函数 (*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs); //main函数返回值 ret = (*env)->ExceptionOccurred(env) == NULL ? 0 : 1; //等待所有非守护线程结束,然后销毁JVM进程 LEAVE(); }
LEAVE是C语言里面的一个宏定义,具体定义如下。
#define LEAVE() \ do { \ if ((*vm)->DetachCurrentThread(vm) ! = JNI_OK) { \ JLI_ReportErrorMessage(JVM_ERROR2); \ ret = 1; \ } \ if (JNI_TRUE) { \ (*vm)->DestroyJavaVM(vm); \ return ret; \ } \ } while (JNI_FALSE)
该宏的作用是创建一个名为DestroyJavaVM的线程,来等待所有用户线程结束。
在Tomcat的NIO实现NioEndpoint中会开启一组接受线程来接受用户的连接请求,以及一组处理线程负责具体处理用户请求,那么这些线程是用户线程还是守护线程呢?下面我们看一下NioEndpoint的startInternal方法。
public void startInternal() throws Exception { if (! running) {
running = true; paused = false; ... //创建处理线程 pollers = new Poller[getPollerThreadCount()]; for (int i=0; i<pollers.length; i++) { pollers[i] = new Poller(); Thread pollerThread = new Thread(pollers[i], getName() + "-ClientPoller-"+i); pollerThread.setPriority(threadPriority); pollerThread.setDaemon(true); //声明为守护线程 pollerThread.start(); } //启动接受线程 startAcceptorThreads(); } protected final void startAcceptorThreads() { int count = getAcceptorThreadCount(); acceptors = new Acceptor[count]; for (int i = 0; i < count; i++) { acceptors[i] = createAcceptor(); String threadName = getName() + "-Acceptor-" + i; acceptors[i].setThreadName(threadName); Thread t = new Thread(acceptors[i], threadName); t.setPriority(getAcceptorThreadPriority()); t.setDaemon(getDaemon()); //设置是否为守护线程,默认为守护线程 t.start(); } } private boolean daemon = true; public void setDaemon(boolean b) { daemon = b; } public boolean getDaemon() { return daemon; }
在如上代码中,在默认情况下,接受线程和处理线程都是守护线程,这意味着当tomcat收到shutdown命令后并且没有其他用户线程存在的情况下tomcat进程会马上消亡,而不会等待处理线程处理完当前的请求。
总结:如果你希望在主线程结束后JVM进程马上结束,那么在创建线程时可以将其设置为守护线程,如果你希望在主线程结束后子线程继续工作,等子线程结束后再让JVM进程结束,那么就将子线程设置为用户线程。