守護線程與用戶線程
1. 前言
本節內容主要是對守護線程與用戶線程進行深入的講解,具體內容點如下:
- 了解守護線程與用戶線程的定義及區別,使我們學習本節內容的基礎知識點;
- 了解守護線程的特點,是我們掌握守護線程的第一步;
- 掌握守護線程的創建,是本節內容的重點;
- 通過守護線程與 JVM 的退出實驗,更加深入的理解守護線程的地位以及作用,為本節內容次重點;
- 了解守護線程的作用及使用場景,為后續開發過程中提供守護線程創建的知識基礎。
2. 守護線程與用戶線程的定義及區別
Java 中的線程分為兩類,分別為 daemon 線程(守護線程〉和 user 線程(用戶線程)。
在 JVM 啟動時會調用 main 函數, main 函數所在的線程就是一個用戶線程,其實在 JVM 內部同時還啟動了好多守護線程,比如垃圾回收線程。
守護線程定義:所謂守護線程,是指在程序運行的時候在后臺提供一種通用服務的線程。比如垃圾回收線程就是一個很稱職的守護者,并且這種線程并不屬于程序中不可或缺的部分。
因此,當所有的非守護線程結束時,程序也就終止了,同時會殺死進程中的所有守護線程。反過來說,只要任何非守護線程還在運行,程序就不會終止。
用戶線程定義:某種意義上的主要用戶線程,只要有用戶線程未執行完畢,JVM 虛擬機不會退出。
區別:在本質上,用戶線程和守護線程并沒有太大區別,唯一的區別就是當最后一個非守護線程結束時,JVM 會正常退出,而不管當前是否有守護線程,也就是說守護線程是否結束并不影響 JVM 的退出。
言外之意,只要有一個用戶線程還沒結束, 正常情況下 JVM 就不會退出。
3. 守護線程的特點
Java 中的守護線程和 Linux 中的守護進程是有些區別的,Linux 守護進程是系統級別的,當系統退出時,才會終止。
而 Java 中的守護線程是 JVM 級別的,當 JVM 中無任何用戶進程時,守護進程銷毀,JVM 退出,程序終止。總結來說,Java 守護進程的最主要的特點有:
- 守護線程是運行在程序后臺的線程;
- 守護線程創建的線程,依然是守護線程;
- 守護線程不會影響 JVM 的退出,當 JVM 只剩余守護線程時,JVM 進行退出;
- 守護線程在 JVM 退出時,自動銷毀。
4. 守護線程的創建
創建方式:將線程轉換為守護線程可以通過調用 Thread 對象的 setDaemon (true) 方法來實現。
創建細節:
- thread.setDaemon (true) 必須在 thread.start () 之前設置,否則會跑出一個 llegalThreadStateException 異常。你不能把正在運行的常規線程設置為守護線程;
- 在 Daemon 線程中產生的新線程也是 Daemon 的;
- 守護線程應該永遠不去訪問固有資源,如文件、數據庫,因為它會在任何時候甚至在一個操作的中間發生中斷。
線程創建代碼示例:
public class DemoTest {
public static void main(String[] args) throws InterruptedException {
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
//代碼執行邏輯
}
});
threadOne.setDaemon(true); //設置threadOne為守護線程
threadOne. start();
}
}
5. 守護線程與 JVM 的退出實驗
為了更好的了解守護線程與 JVM 是否退出的關系,我們首先來設計一個守護線程正在運行,但用戶線程執行完畢導致的 JVM 退出的場景。
場景設計:
- 創建 1 個線程,線程名為 threadOne;
- run 方法線程 sleep 1000 毫秒后,進行求和計算,求解 1 + 2 + 3 + … + 100 的值;
- 將線程 threadOne 設置為守護線程;
- 執行代碼,最終打印的結果;
- 加入 join 方法,強制讓用戶線程等待守護線程 threadOne;
- 執行代碼,最終打印的結果。
期望結果:
- 未加入 join 方法之前,threadOne 不能執行求和邏輯,無打印輸出,因為 main 函數線程執行完畢后,JVM 退出,守護線程也就隨之死亡,無打印結果;
- 加入 join 方法后,可以打印求和結果,因為 main 函數線程需要等待 threadOne 線程執行完畢后才繼續向下執行,main 函數執行完畢,JVM 退出。
Tips:main 函數就是一個用戶線程,main 方法執行時,只有一個用戶線程,如果 main 函數執行完畢,用戶線程銷毀,JVM 退出,此時不會考慮守護線程是否執行完畢,直接退出。
代碼實現 - 不加入 join 方法:
public class DemoTest {
public static void main(String[] args){
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum = sum + i;
}
System.out.println("守護線程,最終求和的值為: " + sum);
}
});
threadOne.setDaemon(true); //設置threadOne為守護線程
threadOne. start();
System.out.println("main 函數線程執行完畢, JVM 退出。");
}
}
執行結果驗證:
main 函數線程執行完畢, JVM 退出。
從結果上可以看到,JVM 退出了,守護線程還沒來得及執行,也就隨著 JVM 的退出而消亡了。
代碼實現 - 加入 join 方法:
public class DemoTest {
public static void main(String[] args){
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum = sum + i;
}
System.out.println("守護線程,最終求和的值為: " + sum);
}
});
threadOne.setDaemon(true); //設置threadOne為守護線程
threadOne. start();
try {
threadOne.join(); // 加入join 方法
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main 函數線程執行完畢, JVM 退出。");
}
}
執行結果驗證:
守護線程,最終求和的值為: 5050
main 函數線程執行完畢, JVM 退出。
從結果來看,守護線程不決定 JVM 的退出,除非強制使用 join 方法使用戶線程等待守護線程的執行結果,但是實際的開發過程中,這樣的操作是不允許的,因為守護線程,默認就是不需要被用戶線程等待的,是服務于用戶線程的。
6. 守護線程的作用及使用場景
作用:我們以 GC 垃圾回收線程舉例,它就是一個經典的守護線程,當我們的程序中不再有任何運行的 Thread, 程序就不會再產生垃圾,垃圾回收器也就無事可做,所以當垃圾回收線程是 JVM 上僅剩的線程時,垃圾回收線程會自動離開。
它始終在低級別的狀態中運行,用于實時監控和管理系統中的可回收資源。
應用場景:
- 為其它線程提供服務支持的情況,可選用守護線程;
- 根據開發需求,程序結束時,這個線程必須正常且立刻關閉,就可以作為守護線程來使用;
- 如果一個正在執行某個操作的線程必須要執行完畢后再釋放,否則就會出現不良的后果的話,那么這個線程就不能是守護線程,而是用戶線程;
- 正常開發過程中,一般心跳監聽,垃圾回收,臨時數據清理等通用服務會選擇守護線程。
7. 小結
掌握用戶線程和守護線程的區別點非常重要,在實際的工作開發中,對一些服務型,通用型的線程服務可以根據需要選擇守護線程進行執行,這樣可以減少 JVM 不可退出的現象,并且可以更好地協調不同種類的線程之間的協作,減少守護線程對高優先級的用戶線程的資源爭奪,使系統更加的穩定。
本節的重中之重是掌握守護線程的創建以及創建需要注意的事項,了解守護線程與用戶線程的區別使我們掌握守護線程的前提。