Java守護(hù)線程實例詳解_動力節(jié)點Java學(xué)院整理
在Java中有兩類線程:User Thread(用戶線程)、Daemon Thread(守護(hù)線程)
用個比較通俗的比如,任何一個守護(hù)線程都是整個JVM中所有非守護(hù)線程的保姆:
只要當(dāng)前JVM實例中尚存在任何一個非守護(hù)線程沒有結(jié)束,守護(hù)線程就全部工作;只有當(dāng)最后一個非守護(hù)線程結(jié)束時,守護(hù)線程隨著JVM一同結(jié)束工作。
Daemon的作用是為其他線程的運行提供便利服務(wù),守護(hù)線程最典型的應(yīng)用就是 GC (垃圾回收器),它就是一個很稱職的守護(hù)者。
User和Daemon兩者幾乎沒有區(qū)別,唯一的不同之處就在于虛擬機(jī)的離開:如果 User Thread已經(jīng)全部退出運行了,只剩下Daemon Thread存在了,虛擬機(jī)也就退出了。 因為沒有了被守護(hù)者,Daemon也就沒有工作可做了,也就沒有繼續(xù)運行程序的必要了。
值得一提的是,守護(hù)線程并非只有虛擬機(jī)內(nèi)部提供,用戶在編寫程序時也可以自己設(shè)置守護(hù)線程。下面的方法就是用來設(shè)置守護(hù)線程的。
Thread daemonTread = new Thread(); // 設(shè)定 daemonThread 為 守護(hù)線程,default false(非守護(hù)線程) daemonThread.setDaemon(true); // 驗證當(dāng)前線程是否為守護(hù)線程,返回 true 則為守護(hù)線程 daemonThread.isDaemon();
這里有幾點需要注意:
(1) thread.setDaemon(true)必須在thread.start()之前設(shè)置,否則會跑出一個IllegalThreadStateException異常。你不能把正在運行的常規(guī)線程設(shè)置為守護(hù)線程。
(2) 在Daemon線程中產(chǎn)生的新線程也是Daemon的。
(3) 不要認(rèn)為所有的應(yīng)用都可以分配給Daemon來進(jìn)行服務(wù),比如讀寫操作或者計算邏輯。
因為你不可能知道在所有的User完成之前,Daemon是否已經(jīng)完成了預(yù)期的服務(wù)任務(wù)。一旦User退出了,可能大量數(shù)據(jù)還沒有來得及讀入或?qū)懗?,計算任?wù)也可能多次運行結(jié)果不一樣。這對程序是毀滅性的。造成這個結(jié)果理由已經(jīng)說過了:一旦所有User Thread離開了,虛擬機(jī)也就退出運行了。
//完成文件輸出的守護(hù)線程任務(wù)
import java.io.*;
class TestRunnable implements Runnable{
public void run(){
try{
Thread.sleep(1000);//守護(hù)線程阻塞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); //設(shè)置守護(hù)線程
thread.start(); //開始執(zhí)行分進(jìn)程
}
}
//運行結(jié)果:文件daemon.txt中沒有"daemon"字符串。
看到了吧,把輸入輸出邏輯包裝進(jìn)守護(hù)線程多么的可怕,字符串并沒有寫入指定文件。原因也很簡單,直到主線程完成,守護(hù)線程仍處于1秒的阻塞狀態(tài)。這個時候主線程很快就運行完了,虛擬機(jī)退出,Daemon停止服務(wù),輸出操作自然失敗了。
public class Test {
public static void main(String args) {
Thread t1 = new MyCommon();
Thread t2 = new Thread(new MyDaemon());
t2.setDaemon(true); //設(shè)置為守護(hù)線程
t2.start();
t1.start();
}
}
class MyCommon extends Thread {
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("線程1第" + i + "次執(zhí)行!");
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 + "次執(zhí)行!");
try {
Thread.sleep(7);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
后臺線程第0次執(zhí)行!
線程1第0次執(zhí)行!
線程1第1次執(zhí)行!
后臺線程第1次執(zhí)行!
后臺線程第2次執(zhí)行!
線程1第2次執(zhí)行!
線程1第3次執(zhí)行!
后臺線程第3次執(zhí)行!
線程1第4次執(zhí)行!
后臺線程第4次執(zhí)行!
后臺線程第5次執(zhí)行!
后臺線程第6次執(zhí)行!
后臺線程第7次執(zhí)行!
Process finished with exit code 0
從上面的執(zhí)行結(jié)果可以看出:
前臺線程是保證執(zhí)行完畢的,后臺線程還沒有執(zhí)行完畢就退出了。
實際上:JRE判斷程序是否執(zhí)行結(jié)束的標(biāo)準(zhǔn)是所有的前臺執(zhí)線程行完畢了,而不管后臺線程的狀態(tài),因此,在使用后臺線程時候一定要注意這個問題。
補(bǔ)充說明:
定義:守護(hù)線程--也稱“服務(wù)線程”,在沒有用戶線程可服務(wù)時會自動離開。
優(yōu)先級:守護(hù)線程的優(yōu)先級比較低,用于為系統(tǒng)中的其它對象和線程提供服務(wù)。
設(shè)置:通過setDaemon(true)來設(shè)置線程為“守護(hù)線程”;將一個用戶線程設(shè)置為守護(hù)線程的方式是在 線程對象創(chuàng)建 之前 用線程對象的setDaemon方法。
example: 垃圾回收線程就是一個經(jīng)典的守護(hù)線程,當(dāng)我們的程序中不再有任何運行的Thread,程序就不會再產(chǎn)生垃圾,垃圾回收器也就無事可做,所以當(dāng)垃圾回收線程是JVM上僅剩的線程時,垃圾回收線程會自動離開。它始終在低級別的狀態(tài)中運行,用于實時監(jiān)控和管理系統(tǒng)中的可回收資源。
生命周期:守護(hù)進(jìn)程(Daemon)是運行在后臺的一種特殊進(jìn)程。它獨立于控制終端并且周期性地執(zhí)行某種任務(wù)或等待處理某些發(fā)生的事件。也就是說守護(hù)線程不依賴于終端,但是依賴于系統(tǒng),與系統(tǒng)“同生共死”。那Java的守護(hù)線程是什么樣子的呢。當(dāng)JVM中所有的線程都是守護(hù)線程的時候,JVM就可以退出了;如果還有一個
或以上的非守護(hù)線程則JVM不會退出。
實際應(yīng)用例子:
在使用長連接的comet服務(wù)端推送技術(shù)中,消息推送線程設(shè)置為守護(hù)線程,服務(wù)于ChatServlet的servlet用戶線程,在servlet的init啟動消息線程,servlet一旦初始化后,一直存在服務(wù)器,servlet摧毀后,消息線程自動退出
容器收到一個Servlet請求,調(diào)度線程從線程池中選出一個工作者線程,將請求傳遞給該工作者線程,然后由該線程來執(zhí)行Servlet的 service方法。當(dāng)這個線程正在執(zhí)行的時候,容器收到另外一個請求,調(diào)度線程同樣從線程池中選出另一個工作者線程來服務(wù)新的請求,容器并不關(guān)心這個請求是否訪問的是同一個Servlet.當(dāng)容器同時收到對同一個Servlet的多個請求的時候,那么這個Servlet的service()方法將在多線程中并發(fā)執(zhí)行。
Servlet容器默認(rèn)采用單實例多線程的方式來處理請求,這樣減少產(chǎn)生Servlet實例的開銷,提升了對請求的響應(yīng)時間,對于Tomcat可以在server.xml中通過<Connector>元素設(shè)置線程池中線程的數(shù)目。
如圖:

為什么要用守護(hù)線程?
我們知道靜態(tài)變量是ClassLoader級別的,如果Web應(yīng)用程序停止,這些靜態(tài)變量也會從JVM中清除。但是線程則是JVM級別的,如果你在Web 應(yīng)用中啟動一個線程,這個線程的生命周期并不會和Web應(yīng)用程序保持同步。也就是說,即使你停止了Web應(yīng)用,這個線程依舊是活躍的。正是因為這個很隱晦 的問題,所以很多有經(jīng)驗的開發(fā)者不太贊成在Web應(yīng)用中私自啟動線程。
如果我們手工使用JDK Timer(Quartz的Scheduler),在Web容器啟動時啟動Timer,當(dāng)Web容器關(guān)閉時,除非你手工關(guān)閉這個Timer,否則Timer中的任務(wù)還會繼續(xù)運行!
下面通過一個小例子來演示這個“詭異”的現(xiàn)象,我們通過ServletContextListener在Web容器啟動時創(chuàng)建一個Timer并周期性地運行一個任務(wù):
//代碼清單StartCycleRunTask:容器監(jiān)聽器
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容器關(guān)閉時執(zhí)行
System.out.println("Web應(yīng)用程序啟動關(guān)閉...");
}
public void contextInitialized(ServletContextEvent arg0) ...{
//②在Web容器啟動時自動執(zhí)行該方法
System.out.println("Web應(yīng)用程序啟動...");
timer = new Timer();//②-1:創(chuàng)建一個Timer,Timer內(nèi)部自動創(chuàng)建一個背景線程
TimerTask task = new SimpleTimerTask();
timer.schedule(task, 1000L, 5000L); //②-2:注冊一個5秒鐘運行一次的任務(wù)
}
}
class SimpleTimerTask extends TimerTask ...{//③任務(wù)
private int count;
public void run() ...{
System.out.println((++count)+"execute task..."+(new Date()));
}
}
在web.xml中聲明這個Web容器監(jiān)聽器:
<?xml version="1.0" encoding="UTF-8"?> <web-app> … <listener> <listener-class>com.bjpowernode.web.StartCycleRunTask</listener-class> </listener> </web-app>
在Tomcat中部署這個Web應(yīng)用并啟動后,你將看到任務(wù)每隔5秒鐘執(zhí)行一次。
運行一段時間后,登錄Tomcat管理后臺,將對應(yīng)的Web應(yīng)用(chapter13)關(guān)閉。
轉(zhuǎn)到Tomcat控制臺,你將看到雖然Web應(yīng)用已經(jīng)關(guān)閉,但Timer任務(wù)還在我行我素地執(zhí)行如故——舞臺已經(jīng)拆除,戲子繼續(xù)表演:
我們可以通過改變清單StartCycleRunTask的代碼,在contextDestroyed(ServletContextEvent arg0)中添加timer.cancel()代碼,在Web容器關(guān)閉后手工停止Timer來結(jié)束任務(wù)。
Spring為JDK Timer和Quartz Scheduler所提供的TimerFactoryBean和SchedulerFactoryBean能夠和Spring容器的生命周期關(guān)聯(lián),在 Spring容器啟動時啟動調(diào)度器,而在Spring容器關(guān)閉時,停止調(diào)度器。所以在Spring中通過這兩個FactoryBean配置調(diào)度器,再從 Spring IoC中獲取調(diào)度器引用進(jìn)行任務(wù)調(diào)度將不會出現(xiàn)這種Web容器關(guān)閉而任務(wù)依然運行的問題。而如果你在程序中直接使用Timer或Scheduler,如不 進(jìn)行額外的處理,將會出現(xiàn)這一問題。
相關(guān)文章
Java監(jiān)聽器ActionListener與MouseListener的執(zhí)行順序說明
這篇文章主要介紹了Java監(jiān)聽器ActionListener與MouseListener的執(zhí)行順序說明,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12
Mybatis-Plus支持GBase8s分頁查詢的實現(xiàn)示例
本文主要介紹了使?Mybatis-Plus?支持?GBase8s?的分頁查詢,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-01-01

