Java并发编程之美
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

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