在Java中有兩類線程:User Thread(用戶線程)、Daemon Thread(守護線程)
用個比較通俗的比如,任何一個守護線程都是整個JVM中所有非守護線程的保姆:
只要當前JVM實例中尚存在任何一個非守護線程沒有結束,守護線程就全部工作;只有當最后一個非守護線程結束時,守護線程隨著JVM一同結束工作。
Daemon的作用是為其他線程的運行提供便利服務,守護線程最典型的應用就是 GC (垃圾回收器),它就是一個很稱職的守護者。
User和Daemon兩者幾乎沒有區別,唯一的不同之處就在于虛擬機的離開:如果 User Thread已經全部退出運行了,只剩下Daemon Thread存在了,虛擬機也就退出了。 因為沒有了被守護者,Daemon也就沒有工作可做了,也就沒有繼續運行程序的必要了。
值得一提的是,守護線程并非只有虛擬機內部提供,用戶在編寫程序時也可以自己設置守護線程。下面的方法就是用來設置守護線程的。
1
2
3
4
5
|
Thread daemonTread = new Thread(); // 設定 daemonThread 為 守護線程,default false(非守護線程) daemonThread.setDaemon( true ); // 驗證當前線程是否為守護線程,返回 true 則為守護線程 daemonThread.isDaemon(); |
這里有幾點需要注意:
(1) thread.setDaemon(true)必須在thread.start()之前設置,否則會跑出一個IllegalThreadStateException異常。你不能把正在運行的常規線程設置為守護線程。
(2) 在Daemon線程中產生的新線程也是Daemon的。
(3) 不要認為所有的應用都可以分配給Daemon來進行服務,比如讀寫操作或者計算邏輯。
因為你不可能知道在所有的User完成之前,Daemon是否已經完成了預期的服務任務。一旦User退出了,可能大量數據還沒有來得及讀入或寫出,計算任務也可能多次運行結果不一樣。這對程序是毀滅性的。造成這個結果理由已經說過了:一旦所有User Thread離開了,虛擬機也就退出運行了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
//完成文件輸出的守護線程任務 import java.io.*; class TestRunnable implements Runnable{ public void run(){ try { Thread.sleep( 1000 ); //守護線程阻塞1秒后運行 File f= new File( "daemon.txt" ); FileOutputStream os= new FileOutputStream(f, true ); os.write( "daemon" .getBytes()); } catch (IOException e1){ e1.printStackTrace(); } catch (InterruptedException e2){ e2.printStackTrace(); } } } public class TestDemo2{ public static void main(String[] args) throws InterruptedException { Runnable tr= new TestRunnable(); Thread thread= new Thread(tr); thread.setDaemon( true ); //設置守護線程 thread.start(); //開始執行分進程 } } //運行結果:文件daemon.txt中沒有"daemon"字符串。 |
看到了吧,把輸入輸出邏輯包裝進守護線程多么的可怕,字符串并沒有寫入指定文件。原因也很簡單,直到主線程完成,守護線程仍處于1秒的阻塞狀態。這個時候主線程很快就運行完了,虛擬機退出,Daemon停止服務,輸出操作自然失敗了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
public class Test { public static void main(String args) { Thread t1 = new MyCommon(); Thread t2 = new Thread( new MyDaemon()); t2.setDaemon( true ); //設置為守護線程 t2.start(); t1.start(); } } class MyCommon extends Thread { public void run() { for ( int i = 0 ; i < 5 ; i++) { System.out.println( "線程1第" + i + "次執行!" ); try { Thread.sleep( 7 ); } catch (InterruptedException e) { e.printStackTrace(); } } } } class MyDaemon implements Runnable { public void run() { for ( long i = 0 ; i < 9999999L; i++) { System.out.println( "后臺線程第" + i + "次執行!" ); try { Thread.sleep( 7 ); } catch (InterruptedException e) { e.printStackTrace(); } } } } |
后臺線程第0次執行!
線程1第0次執行!
線程1第1次執行!
后臺線程第1次執行!
后臺線程第2次執行!
線程1第2次執行!
線程1第3次執行!
后臺線程第3次執行!
線程1第4次執行!
后臺線程第4次執行!
后臺線程第5次執行!
后臺線程第6次執行!
后臺線程第7次執行!
Process finished with exit code 0
從上面的執行結果可以看出:
前臺線程是保證執行完畢的,后臺線程還沒有執行完畢就退出了。
實際上:JRE判斷程序是否執行結束的標準是所有的前臺執線程行完畢了,而不管后臺線程的狀態,因此,在使用后臺線程時候一定要注意這個問題。
補充說明:
定義:守護線程--也稱“服務線程”,在沒有用戶線程可服務時會自動離開。
優先級:守護線程的優先級比較低,用于為系統中的其它對象和線程提供服務。
設置:通過setDaemon(true)來設置線程為“守護線程”;將一個用戶線程設置為守護線程的方式是在 線程對象創建 之前 用線程對象的setDaemon方法。
example: 垃圾回收線程就是一個經典的守護線程,當我們的程序中不再有任何運行的Thread,程序就不會再產生垃圾,垃圾回收器也就無事可做,所以當垃圾回收線程是JVM上僅剩的線程時,垃圾回收線程會自動離開。它始終在低級別的狀態中運行,用于實時監控和管理系統中的可回收資源。
生命周期:守護進程(Daemon)是運行在后臺的一種特殊進程。它獨立于控制終端并且周期性地執行某種任務或等待處理某些發生的事件。也就是說守護線程不依賴于終端,但是依賴于系統,與系統“同生共死”。那Java的守護線程是什么樣子的呢。當JVM中所有的線程都是守護線程的時候,JVM就可以退出了;如果還有一個
或以上的非守護線程則JVM不會退出。
實際應用例子:
在使用長連接的comet服務端推送技術中,消息推送線程設置為守護線程,服務于ChatServlet的servlet用戶線程,在servlet的init啟動消息線程,servlet一旦初始化后,一直存在服務器,servlet摧毀后,消息線程自動退出
容器收到一個Servlet請求,調度線程從線程池中選出一個工作者線程,將請求傳遞給該工作者線程,然后由該線程來執行Servlet的 service方法。當這個線程正在執行的時候,容器收到另外一個請求,調度線程同樣從線程池中選出另一個工作者線程來服務新的請求,容器并不關心這個請求是否訪問的是同一個Servlet.當容器同時收到對同一個Servlet的多個請求的時候,那么這個Servlet的service()方法將在多線程中并發執行。
Servlet容器默認采用單實例多線程的方式來處理請求,這樣減少產生Servlet實例的開銷,提升了對請求的響應時間,對于Tomcat可以在server.xml中通過<Connector>元素設置線程池中線程的數目。
如圖:
為什么要用守護線程?
我們知道靜態變量是ClassLoader級別的,如果Web應用程序停止,這些靜態變量也會從JVM中清除。但是線程則是JVM級別的,如果你在Web 應用中啟動一個線程,這個線程的生命周期并不會和Web應用程序保持同步。也就是說,即使你停止了Web應用,這個線程依舊是活躍的。正是因為這個很隱晦 的問題,所以很多有經驗的開發者不太贊成在Web應用中私自啟動線程。
如果我們手工使用JDK Timer(Quartz的Scheduler),在Web容器啟動時啟動Timer,當Web容器關閉時,除非你手工關閉這個Timer,否則Timer中的任務還會繼續運行!
下面通過一個小例子來演示這個“詭異”的現象,我們通過ServletContextListener在Web容器啟動時創建一個Timer并周期性地運行一個任務:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
//代碼清單StartCycleRunTask:容器監聽器 package com.bjpowernode.web; import java.util.Date; import java.util.Timer; import java.util.TimerTask; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; public class StartCycleRunTask implements ServletContextListener ...{ private Timer timer; public void contextDestroyed(ServletContextEvent arg0) ...{ // ②該方法在Web容器關閉時執行 System.out.println( "Web應用程序啟動關閉..." ); } public void contextInitialized(ServletContextEvent arg0) ...{ //②在Web容器啟動時自動執行該方法 System.out.println( "Web應用程序啟動..." ); timer = new Timer(); //②-1:創建一個Timer,Timer內部自動創建一個背景線程 TimerTask task = new SimpleTimerTask(); timer.schedule(task, 1000L, 5000L); //②-2:注冊一個5秒鐘運行一次的任務 } } class SimpleTimerTask extends TimerTask ...{ //③任務 private int count; public void run() ...{ System.out.println((++count)+ "execute task..." +( new Date())); } } |
在web.xml中聲明這個Web容器監聽器:
1
2
3
4
5
6
7
|
<?xml version= "1.0" encoding= "UTF-8" ?> <web-app> … <listener> <listener- class >com.bjpowernode.web.StartCycleRunTask</listener- class > </listener> </web-app> |
在Tomcat中部署這個Web應用并啟動后,你將看到任務每隔5秒鐘執行一次。
運行一段時間后,登錄Tomcat管理后臺,將對應的Web應用(chapter13)關閉。
轉到Tomcat控制臺,你將看到雖然Web應用已經關閉,但Timer任務還在我行我素地執行如故——舞臺已經拆除,戲子繼續表演:
我們可以通過改變清單StartCycleRunTask的代碼,在contextDestroyed(ServletContextEvent arg0)中添加timer.cancel()代碼,在Web容器關閉后手工停止Timer來結束任務。
Spring為JDK Timer和Quartz Scheduler所提供的TimerFactoryBean和SchedulerFactoryBean能夠和Spring容器的生命周期關聯,在 Spring容器啟動時啟動調度器,而在Spring容器關閉時,停止調度器。所以在Spring中通過這兩個FactoryBean配置調度器,再從 Spring IoC中獲取調度器引用進行任務調度將不會出現這種Web容器關閉而任務依然運行的問題。而如果你在程序中直接使用Timer或Scheduler,如不 進行額外的處理,將會出現這一問題。