後臺線程(守護線程)

所謂的後臺線程,是指在程序運行的時候在後臺提供一種通用服務的線程,並且這種線程並不屬於程序中不可或缺的部分。因此當所有的非後臺線程結束時,程序也就終止了,同時會殺死所有後臺線程。反過來說,只要有任何非後臺線程(用戶線程)還在運行,程序就不會終止。後臺線程在不執行finally子句的情況下就會終止其run方法後臺線程創建的子線程也是後臺線程。

下面是一個後臺線程的示例:

[java] view plaincopy
 
  1. <span style="font-size:16px;">package demo.thread;  
  2.   
  3. import java.util.concurrent.TimeUnit;  
  4.   
  5. public class DaemonDemo implements Runnable {  
  6.     @Override  
  7.     public void run() {  
  8.         try {  
  9.             while (true) {  
  10.                 Thread.sleep(1000);  
  11.                 System.out.println("#" + Thread.currentThread().getName());  
  12.             }  
  13.         } catch (InterruptedException e) {  
  14.             e.printStackTrace();  
  15.         } finally {// 後臺線程不執行finally子句  
  16.             System.out.println("finally ");  
  17.         }  
  18.     }  
  19.   
  20.     public static void main(String[] args) {  
  21.         for (int i = 0; i < 10; i++) {  
  22.             Thread daemon = new Thread(new DaemonDemo());  
  23.             // 必須在start之前設置爲後臺線程  
  24.             daemon.setDaemon(true);  
  25.             daemon.start();  
  26.         }  
  27.         System.out.println("All daemons started");  
  28.         try {  
  29.             TimeUnit.MILLISECONDS.sleep(1000);  
  30.         } catch (InterruptedException e) {  
  31.             // TODO Auto-generated catch block  
  32.             e.printStackTrace();  
  33.         }  
  34.     }  
  35. }  
  36. </span>  


 

運行結果:

All daemons started
#Thread-2
#Thread-3
#Thread-1
#Thread-0
#Thread-9
#Thread-6
#Thread-8
#Thread-5
#Thread-7
#Thread-4

分析:從結果可以看出,十個子線程並沒有無線循環的打印,而是在主線程(main())退出後,JVM強制關閉所有後臺線程。而不會有任何希望出現的確認形式,如finally子句不執行。

 

在Java中有兩類線程:User Thread(用戶線程)、Daemon Thread(守護線程) 

用個比較通俗的比如,任何一個守護線程都是整個JVM中所有非守護線程的保姆:

只要當前JVM實例中尚存在任何一個非守護線程沒有結束,守護線程就全部工作;只有當最後一個非守護線程結束時,守護線程隨着JVM一同結束工作。
Daemon的作用是爲其他線程的運行提供便利服務,守護線程最典型的應用就是 GC (垃圾回收器),它就是一個很稱職的守護者。

User和Daemon兩者幾乎沒有區別,唯一的不同之處就在於虛擬機的離開:如果 User Thread已經全部退出運行了,只剩下Daemon Thread存在了,虛擬機也就退出了。 因爲沒有了被守護者,Daemon也就沒有工作可做了,也就沒有繼續運行程序的必要了。


值得一提的是,守護線程並非只有虛擬機內部提供,用戶在編寫程序時也可以自己設置守護線程。下面的方法就是用來設置守護線程的。 

[java] view plaincopy
 
  1. Thread daemonTread = new Thread();  
  2.    
  3.   // 設定 daemonThread 爲 守護線程,default false(非守護線程)  
  4.  daemonThread.setDaemon(true);  
  5.    
  6.  // 驗證當前線程是否爲守護線程,返回 true 則爲守護線程  
  7.  daemonThread.isDaemon();  


這裏有幾點需要注意: 

(1) thread.setDaemon(true)必須在thread.start()之前設置,否則會跑出一個IllegalThreadStateException異常。你不能把正在運行的常規線程設置爲守護線程。
(2) 在Daemon線程中產生的新線程也是Daemon的。 
(3) 不要認爲所有的應用都可以分配給Daemon來進行服務,比如讀寫操作或者計算邏輯。 

因爲你不可能知道在所有的User完成之前,Daemon是否已經完成了預期的服務任務。一旦User退出了,可能大量數據還沒有來得及讀入或寫出,計算任務也可能多次運行結果不一樣。這對程序是毀滅性的。造成這個結果理由已經說過了:一旦所有User Thread離開了,虛擬機也就退出運行了。 

 

[java] view plaincopy
 
  1. //完成文件輸出的守護線程任務  
  2. import java.io.*;     
  3.     
  4. class TestRunnable implements Runnable{     
  5.     public void run(){     
  6.                try{     
  7.                   Thread.sleep(1000);//守護線程阻塞1秒後運行     
  8.                   File f=new File("daemon.txt");     
  9.                   FileOutputStream os=new FileOutputStream(f,true);     
  10.                   os.write("daemon".getBytes());     
  11.            }     
  12.                catch(IOException e1){     
  13.           e1.printStackTrace();     
  14.                }     
  15.                catch(InterruptedException e2){     
  16.                   e2.printStackTrace();     
  17.            }     
  18.     }     
  19. }     
  20. public class TestDemo2{     
  21.     public static void main(String[] args) throws InterruptedException     
  22.     {     
  23.         Runnable tr=new TestRunnable();     
  24.         Thread thread=new Thread(tr);     
  25.                 thread.setDaemon(true); //設置守護線程     
  26.         thread.start(); //開始執行分進程     
  27.     }     
  28. }     
  29. //運行結果:文件daemon.txt中沒有"daemon"字符串。  


看到了吧,把輸入輸出邏輯包裝進守護線程多麼的可怕,字符串並沒有寫入指定文件。原因也很簡單,直到主線程完成,守護線程仍處於1秒的阻塞狀態。這個時候主線程很快就運行完了,虛擬機退出,Daemon停止服務,輸出操作自然失敗了。

 

 

[java] view plaincopy
 
  1. public class Test {  
  2.   public static void main(String args) {  
  3.   Thread t1 = new MyCommon();  
  4.   Thread t2 = new Thread(new MyDaemon());  
  5.   t2.setDaemon(true); //設置爲守護線程  
  6.   t2.start();  
  7.   t1.start();  
  8.   }  
  9.   }  
  10.   class MyCommon extends Thread {  
  11.   public void run() {  
  12.   for (int i = 0; i < 5; i++) {  
  13.   System.out.println("線程1第" + i + "次執行!");  
  14.   try {  
  15.   Thread.sleep(7);  
  16.   } catch (InterruptedException e) {  
  17.   e.printStackTrace();  
  18.   }  
  19.   }  
  20.   }  
  21.   }  

 

[html] view plaincopy
 
  1. class MyDaemon implements Runnable {  
  2.   public void run() {  
  3.   for (long i = 0; i < 9999999L; i++) {  
  4.   System.out.println("後臺線程第" + i + "次執行!");  
  5.   try {  
  6.   Thread.sleep(7);  
  7.   } catch (InterruptedException e) {  
  8.   e.printStackTrace();  
  9.   }  
  10.   }  
  11.   }  
  12.   }  


後臺線程第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並週期性地運行一個任務:  

[java] view plaincopy
 
  1. //代碼清單StartCycleRunTask:容器監聽器  
  2. package com.baobaotao.web;  
  3. import java.util.Date;  
  4. import java.util.Timer;  
  5. import java.util.TimerTask;  
  6. import javax.servlet.ServletContextEvent;  
  7. import javax.servlet.ServletContextListener;  
  8. public class StartCycleRunTask implements ServletContextListener ...{  
  9.     private Timer timer;  
  10.     public void contextDestroyed(ServletContextEvent arg0) ...{  
  11.         // ②該方法在Web容器關閉時執行  
  12.         System.out.println("Web應用程序啓動關閉...");  
  13.     }  
  14.     public void contextInitialized(ServletContextEvent arg0) ...{  
  15.          //②在Web容器啓動時自動執行該方法  
  16.         System.out.println("Web應用程序啓動...");  
  17.         timer = new Timer();//②-1:創建一個Timer,Timer內部自動創建一個背景線程  
  18.         TimerTask task = new SimpleTimerTask();  
  19.         timer.schedule(task, 1000L, 5000L); //②-2:註冊一個5秒鐘運行一次的任務  
  20.     }  
  21. }  
  22. class SimpleTimerTask extends TimerTask ...{//③任務  
  23.     private int count;  
  24.     public void run() ...{  
  25.         System.out.println((++count)+"execute task..."+(new Date()));  
  26.     }  
  27. }  


在web.xml中聲明這個Web容器監聽器:<?xml version="1.0" encoding="UTF-8"?>
<web-app> 
… 
<listener> 
<listener-class>com.baobaotao.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,如不 進行額外的處理,將會出現這一問題。 

 

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