1. 程式人生 > >java併發程式設計:之守護執行緒(Daemon)

java併發程式設計:之守護執行緒(Daemon)

在Java中有兩類執行緒:User Thread(使用者執行緒)、Daemon Thread(守護執行緒) 

用個比較通俗的比喻,任何一個守護執行緒都是整個JVM中所有非守護執行緒的保姆:

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

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

值得一提的是,守護執行緒並非只有虛擬機器內部提供,使用者在編寫程式時也可以自己設定守護執行緒。下面的方法就是用來設定守護執行緒的。

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離開了,虛擬機器也就退出執行了。 

//完成檔案輸出的守護執行緒任務  
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停止服務,輸出操作自然失敗了。

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判斷程式是否執行結束的標準是所有的前臺執執行緒行完畢了,而不管後臺執行緒的狀態,因此,在使用後臺縣城時候一定要注意這個問題。 

example: 垃圾回收執行緒就是一個經典的守護執行緒,當我們的程式中不再有任何執行的Thread,程式就不會再產生垃圾,垃圾回收器也就無事可做,所以當垃圾回收執行緒是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並週期性地執行一個任務:  

//程式碼清單StartCycleRunTask:容器監聽器  
package com.baobaotao.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容器監聽器:<?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,如不 進行額外的處理,將會出現這一問題。