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