額!Java中用戶線程和守護線程區別這麼大?

在 Java 語言中線程分爲兩類:用戶線程和守護線程,而二者之間的區別卻鮮有人知,所以本文磊哥帶你來看二者之間的區別,以及守護線程需要注意的一些事項。

1.默認用戶線程

Java 語言中無論是線程還是線程池,默認都是用戶線程,因此用戶線程也被成爲普通線程。

以線程爲例,想要查看線程是否爲守護線程只需通過調用 isDaemon() 方法查詢即可,如果查詢的值爲 false 則表示不爲守護線程,自然也就屬於用戶線程了,如下代碼所示:

public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("我是子線程");
        }
    });
    System.out.println("子線程==守護線程:" + thread.isDaemon());
    System.out.println("主線程==守護線程:" + Thread.currentThread().isDaemon());
}

以上程序的執行結果爲:
image.png
從上述結果可以看出,默認情況下主線程和創建的新線程都爲用戶線程

PS:Thread.currentThread() 的意思是獲取執行當前代碼的線程實例。

2.主動修改爲守護線程

守護線程(Daemon Thread)也被稱之爲後臺線程或服務線程,守護線程是爲用戶線程服務的,當程序中的用戶線程全部執行結束之後,守護線程也會跟隨結束。

守護線程的角色就像“服務員”,而用戶線程的角色就像“顧客”,當“顧客”全部走了之後(全部執行結束),那“服務員”(守護線程)也就沒有了存在的意義,所以當一個程序中的全部用戶線程都結束執行之後,那麼無論守護線程是否還在工作都會隨着用戶線程一塊結束,整個程序也會隨之結束運行。

那如何將默認的用戶線程修改爲守護線程呢?

這個問題要分爲兩種情況來回答,首先如果是線程,則可以通過設置 setDaemon(true) 方法將用戶線程直接修改爲守護線程,而如果是線程池則需要通過 ThreadFactory 將線程池中的每個線程都爲守護線程纔行,接下來我們分別來實現一下。

2.1 設置線程爲守護線程

如果使用的是線程,可以通過 setDaemon(true) 方法將線程類型更改爲守護線程,如下代碼所示:

 public static void main(String[] args) throws InterruptedException {
     Thread thread = new Thread(new Runnable() {
         @Override
         public void run() {
             System.out.println("我是子線程");
         }
     });
     // 設置子線程爲守護線程
     thread.setDaemon(true);
     System.out.println("子線程==守護線程:" + thread.isDaemon());
     System.out.println("主線程==守護線程:" + Thread.currentThread().isDaemon());
 }

以上程序的執行結果爲:
image.png

2.2 設置線程池爲守護線程

要把線程池設置爲守護線程相對來說麻煩一些,需要將線程池中的所有線程都設置成守護線程,這個時候就需要使用 ThreadFactory 來定義線程池中每個線程的線程類型了,具體實現代碼如下:

// 創建固定個數的線程池
ExecutorService threadPool = Executors.newFixedThreadPool(10, new ThreadFactory() {
    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r);
        // 設置線程爲守護線程
        t.setDaemon(false);
        return t;
    }
});

如下圖所示:
image.png
如上圖所示,可以看出,整個程序中有 10 個守護線程都是我創建的。其他幾種創建線程池的設置方式類似,都是通過 ThreadFactory 統一設置的,這裏就不一一列舉了。

3.守護線程 VS 用戶線程

通過前面的學習我們可以創建兩種不同的線程類型了,那二者有什麼差異呢?接下來我們使用一個小示例來看一下。

下面我們創建一個線程,分別將這個線程設置爲用戶線程和守護線程,在每個線程中執行一個 for 循環,總共執行 10 次信息打印,每次打印之後休眠 100 毫秒,來觀察程序的運行結果。

3.1 用戶線程

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

/**
 * Author:Java中文社羣
 */
public class DaemonExample {
    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 次打印之後纔會正常結束進程。

3.2 守護線程

/**
 * Author:Java中文社羣
 */
public class DaemonExample {
    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 次之後再進行關閉,而是當主線程結束之後,守護線程只執行了一次循環就結束運行了,由此可以看出守護線程和用戶線程的不同。

3.3 小結

守護線程是爲用戶線程服務的,當一個程序中的所有用戶線程都執行完成之後程序就會結束運行,程序結束運行時不會管守護線程是否正在運行,由此我們可以看出守護線程在 Java 體系中權重是比較低的。

4.守護線程注意事項

守護線程的使用需要注意以下三個問題:

  1. 守護線程的設置 setDaemon(true) 必須要放在線程的 start() 之前,否則程序會報錯。
  2. 在守護線程中創建的所有子線程都是守護線程。
  3. 使用 jojn() 方法會等待一個線程執行完,無論此線程是用戶線程還是守護線程。

接下來我們分別演示一下,以上的注意事項。

4.1 setDaemon 執行順序

當我們將 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() 之後,不但程序的執行會報錯,而且設置的守護線程也不會生效。

4.2 守護線程的子線程

public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            Thread thread2 = new Thread(new Runnable() {
                @Override
                public void run() {

                }
            });
            System.out.println("守護線程的子線程 thread2 isDaemon:" +
                               thread2.isDaemon());
        }
    });
    // 設置爲守護線程
    thread.setDaemon(true);
    // 啓動線程
    thread.start();

    Thread.sleep(1000);
}

以上程序執行結果如下:
image.png
從上述結果可以看出,守護線程中創建的子線程,默認情況下也屬於守護線程

4.3 join 與守護線程

通過 3.2 部分的內容我們可以看出,默認情況下程序結束並不會等待守護線程執行完,而當我們調用線程的等待方法 join() 時,執行的結果就會和 3.2 的結果有所不同,下面我們一起來看吧,示例代碼如下:

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();
    // 等待線程執行完
    thread.join();
    System.out.println("子線程==守護線程:" + thread.isDaemon());
    System.out.println("主線程==守護線程:" + Thread.currentThread().isDaemon());
}

以上程序執行結果如下:
image.png
通過上述結果我們可以看出,即使是守護線程,當程序中調用 join() 方法時,程序依然會等待守護線程執行完成之後再結束進程。

5.守護線程應用場景

守護線程的典型應用場景就是垃圾回收線程,當然還有一些場景也非常適合使用守護線程,比如服務器端的健康檢測功能,對於一個服務器來說健康檢測功能屬於非核心非主流的服務業務,像這種爲了主要業務服務的業務功能就非常合適使用守護線程,當程序中的主要業務都執行完成之後,服務業務也會跟隨者一起銷燬。

6.守護線程的執行優先級

首先來說,線程的類型(用戶線程或守護線程)並不影響線程執行的優先級,如下代碼所示,定義一個用戶線程和守護線程,分別執行 10 萬次循環,通過觀察最後的打印結果來確認線程類型對程序執行優先級的影響。

public class DaemonExample {
    private static final int count = 100000;
    public static void main(String[] args) throws InterruptedException {
        // 定義任務
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < count; i++) {
                    System.out.println("執行線程:" + Thread.currentThread().getName());
                }
            }
        };
        // 創建守護線程 t1
        Thread t1 = new Thread(runnable, "t1");
        // 設置爲守護線程
        t1.setDaemon(true);
        // 啓動線程
        t1.start();
        // 創建用戶線程 t2
        Thread t2 = new Thread(runnable, "t2");
        // 啓動線程
        t2.start();
    }
}

以上程序執行結果如下:
image.png
通過上述結果可以看出,線程的類型不管是守護線程還是用戶線程對程序執行的優先級是沒有任何影響的,而當我們將 t2 的優先級調整爲最大時,整個程序的運行結果就完全不同了,如下代碼所示:

public class DaemonExample {
    private static final int count = 100000;
    public static void main(String[] args) throws InterruptedException {
        // 定義任務
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < count; i++) {
                    System.out.println("執行線程:" + Thread.currentThread().getName());
                }
            }
        };
        // 創建守護線程 t1
        Thread t1 = new Thread(runnable, "t1");
        // 設置爲守護線程
        t1.setDaemon(true);
        // 啓動線程
        t1.start();
        // 創建用戶線程 t2
        Thread t2 = new Thread(runnable, "t2");
        // 設置 t2 的優先級爲最高
        t2.setPriority(Thread.MAX_PRIORITY);
        // 啓動線程
        t2.start();
    }
}

以上程序執行結果如下:
image.png
通過上述的結果可以看出,程序的類型和程序執行的優先級是沒有任何關係,當新創建的線程默認的優先級都是 5 時,無論是守護線程還是用戶線程,它們執行的優先級都是相同的,當將二者的優先級設置不同時,執行的結果也會隨之改變(優先級設置的越高,最早被執行的概率也越大)。

7.總結

在 Java 語言中線程分爲用戶線程和守護線程,守護線程是用來爲用戶線程服務的,當一個程序中的所有用戶線程都結束之後,無論守護線程是否在工作都會跟隨用戶線程一起結束。守護線程從業務邏輯層面來看權重比較低,但對於線程調度器來說無論是守護線程還是用戶線程,在優先級相同的情況下被執行的概率都是相同的。守護線程的經典使用場景是垃圾回收線程,守護線程中創建的線程默認情況下也都是守護線程。

關注公號「Java中文社羣」查看更多有意思、漲知識的併發編程文章。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章