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 JNICALLJavaMain(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 进程结束,那么就将子线程设置为用户线程。
