面試突擊31:什麼是守護執行緒?它和用戶執行緒有什麼區別?

在 Java 語言中,執行緒分為兩類:用戶執行緒和守護執行緒,默認情況下我們創建的執行緒或執行緒池都是用戶執行緒,所以用戶執行緒也被稱之為普通執行緒。

想要查看執行緒到底是用戶執行緒還是守護執行緒,可以通過 Thread.isDaemon() 方法來判斷,如果返回的結果是 true 則為守護執行緒,反之則為用戶執行緒。

我們來測試一下默認情況下執行緒和執行緒池屬於哪種執行緒類型?測試程式碼如下:

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 執行緒類型:守護執行緒 OR 用戶執行緒
 */
public class ThreadType {
    public static void main(String[] args) {
        // 創建執行緒
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                //...
            }
        });
        // 創建執行緒池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 10,
                0, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100));
        threadPool.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("ThreadPool 執行緒類型:" +
                        (Thread.currentThread().isDaemon() == true ? "守護執行緒" : "用戶執行緒"));
            }
        });
        System.out.println("Thread 執行緒類型:" +
                (thread.isDaemon() == true ? "守護執行緒" : "用戶執行緒"));
        System.out.println("main 執行緒類型:" +
                (Thread.currentThread().isDaemon() == true ? "守護執行緒" : "用戶執行緒"));
    }
}

以上程式的執行結果如下圖所示:
image.png
從上述結果可以看出,默認情況下創建的執行緒和執行緒池都是用戶執行緒。

守護執行緒定義

守護執行緒(Daemon Thread)也被稱之為後台執行緒或服務執行緒,守護執行緒是為用戶執行緒服務的,當程式中的用戶執行緒全部執行結束之後,守護執行緒也會跟隨結束。
守護執行緒的角色就像「服務員」,而用戶執行緒的角色就像「顧客」,當「顧客」全部走了之後(全部執行結束),那「服務員」(守護執行緒)也就沒有了存在的意義,所以當一個程式中的全部用戶執行緒都結束執行之後,那麼無論守護執行緒是否還在工作都會隨著用戶執行緒一塊結束,整個程式也會隨之結束運行。

創建守護執行緒

我們可以通過 Thread.setDaemon(true) 方法將執行緒設置為守護執行緒,比如以下程式碼的實現:

public static void main(String[] args) {
    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            //...
        }
    });
    // 設置執行緒為守護執行緒
    thread.setDaemon(true);
    System.out.println("Thread 執行緒類型:" +
                       (thread.isDaemon() == true ? "守護執行緒" : "用戶執行緒"));
    System.out.println("main 執行緒類型:" +
                       (Thread.currentThread().isDaemon() == true ? "守護執行緒" : "用戶執行緒"));
}

以上程式的執行結果如下圖所示:
image.png

將執行緒池設置為守護執行緒

要把執行緒池設置為守護執行緒相對來說麻煩一些,需要將執行緒池中的所有執行緒都設置成守護執行緒,這個時候就需要使用執行緒工廠 ThreadFactory 來設置了(執行緒池中的所有執行緒都是通過執行緒工廠創建的),它的具體實現程式碼如下:

public static void main(String[] args) throws InterruptedException {
    // 執行緒工廠(設置守護執行緒)
    ThreadFactory threadFactory = new ThreadFactory() {
        @Override
        public Thread newThread(Runnable r) {
            Thread thread = new Thread(r);
            // 設置為守護執行緒
            thread.setDaemon(true);
            return thread;
        }
    };
    // 創建執行緒池
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 10,
                                                           0, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100), threadFactory);
    threadPool.submit(new Runnable() {
        @Override
        public void run() {
            System.out.println("ThreadPool 執行緒類型:" +
                               (Thread.currentThread().isDaemon() == true ? "守護執行緒" : "用戶執行緒"));
        }
    });
    Thread.sleep(2000);
}

以上程式的執行結果如下圖所示:
image.png

守護執行緒 VS 用戶執行緒

通過前面的內容我們了解了什麼是用戶執行緒和守護執行緒了,那二者有什麼區別呢?接下來我們用一個小示例來觀察一下。
接下來我們將創建一個執行緒,分別將這個執行緒設置為用戶執行緒和守護執行緒,在每個執行緒中執行一個 for 循環,總共執行 10 次資訊列印,每次列印之後休眠 100 毫秒,來觀察程式的運行結果。

用戶執行緒

新建的執行緒默認就是用戶執行緒,因此我們無需對執行緒進行任何特殊的處理,執行 for 循環即可(總共執行 10 次資訊列印,每次列印之後休眠 100 毫秒),實現程式碼如下:

public static void main(String[] args) throws InterruptedException {
    // 創建用戶執行緒
    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 1; i <= 10; i++) {
                // 列印 i 資訊
                System.out.println("i:" + i);
                try {
                    // 休眠 100 毫秒
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    });
    // 啟動執行緒
    thread.start();
}

以上程式的執行結果如下圖所示:
image.png
從上述結果可以看出,當程式執行完 10 次列印之後才會正常結束進程。

守護執行緒

public static void main(String[] args) throws InterruptedException {
    // 創建守護執行緒
    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 1; i <= 10; i++) {
                // 列印 i 資訊
                System.out.println("i:" + i);
                try {
                    // 休眠 100 毫秒
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    });
    // 設置為守護執行緒
    thread.setDaemon(true);
    // 啟動執行緒
    thread.start();
}

以上程式執行結果如下圖所示:
image.png
從上述結果可以看出,當執行緒設置為守護執行緒之後,整個程式不會等守護執行緒 for 循環 10 次之後再進行關閉,而是當主執行緒結束之後,守護執行緒一次循環都沒執行就結束了,由此可以看出守護執行緒和用戶執行緒的不同。

守護執行緒注意事項

守護執行緒的設置 setDaemon(true) 必須要放在執行緒的 start() 之前,否則程式會報錯。也就是說在運行執行緒之前,一定要先確定執行緒的類型,並且執行緒運行之後是不允許修改執行緒的類型的。
接下來我們來演示一下,如果在程式運行執行再設置執行緒的類型會出現什麼問題?演示程式碼如下:

public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 1; i <= 10; i++) {
                // 列印 i 資訊
                System.out.println("i:" + i + ",isDaemon:" +
                            Thread.currentThread().isDaemon());
                try {
                    // 休眠 100 毫秒
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    });
    // 啟動執行緒
    thread.start();
    // 設置為守護執行緒
    thread.setDaemon(true);
}

以上程式執行結果如下圖所示:
image.png
從上述結果可以看出,當我們將 setDaemon(true) 設置在 start() 之後,不但程式的執行會報錯,而且設置的守護執行緒也不會生效。

總結

在 Java 語言中執行緒分為兩類:用戶執行緒和守護執行緒,默認情況下我們創建的執行緒或執行緒池都是用戶執行緒,守護執行緒是為用戶執行緒服務的,當一個程式中的所有用戶執行緒都執行完成之後程式就會結束運行,程式結束運行時不會管守護執行緒是否正在運行,由此我們可以看出守護執行緒在 Java 體系中權重是比較低的,這就是守護執行緒和用戶執行緒的區別。

是非審之於己,毀譽聽之於人,得失安之於數。

公眾號:Java面試真題解析

面試合集://gitee.com/mydb/interview

Tags: