啃碎併發(二):Java執行緒的生命週期

猿碼道發表於2018-02-01

0 前言

當執行緒被建立並啟動以後,它既不是一啟動就進入了執行狀態,也不是一直處於執行狀態。線上程的生命週期中,它要經過 新建(New)、就緒(Runnable)、執行(Running)、阻塞(Blocked)和死亡(Dead)5種狀態。尤其是當執行緒啟動以後,它不可能一直"霸佔"著CPU獨自執行,所以CPU需要在多條執行緒之間切換,於是 執行緒狀態也會多次在執行、阻塞之間切換

執行緒狀態轉換關係

1 新建(New)狀態

當程式使用new關鍵字建立了一個執行緒之後,該執行緒就處於 新建狀態,此時的執行緒情況如下:

  1. 此時JVM為其分配記憶體,並初始化其成員變數的值
  2. 此時執行緒物件沒有表現出任何執行緒的動態特徵,程式也不會執行執行緒的執行緒執行體;

2 就緒(Runnable)狀態

當執行緒物件呼叫了start()方法之後,該執行緒處於 就緒狀態。此時的執行緒情況如下:

  1. 此時JVM會為其 建立方法呼叫棧和程式計數器
  2. 該狀態的執行緒一直處於 執行緒就緒佇列(儘管是採用佇列形式,事實上,把它稱為可執行池而不是可執行佇列。因為CPU的排程不一定是按照先進先出的順序來排程的),執行緒並沒有開始執行;
  3. 此時執行緒 等待系統為其分配CPU時間片,並不是說執行了start()方法就立即執行;

呼叫start()方法與run()方法,對比如下:

  1. 呼叫start()方法來啟動執行緒,系統會把該run()方法當成執行緒執行體來處理。但如果直接呼叫執行緒物件的run()方法,則run()方法立即就會被執行,而且在run()方法返回之前其他執行緒無法併發執行。也就是說,系統把執行緒物件當成一個普通物件,而run()方法也是一個普通方法,而不是執行緒執行體
  2. 需要指出的是,呼叫了執行緒的run()方法之後,該執行緒已經不再處於新建狀態,不要再次呼叫執行緒物件的start()方法。只能對處於新建狀態的執行緒呼叫start()方法,否則將引發IllegaIThreadStateExccption異常

如何讓子執行緒呼叫start()方法之後立即執行而非"等待執行":

程式可以使用Thread.sleep(1) 來讓當前執行的執行緒(主執行緒)睡眠1毫秒,1毫秒就夠了,因為在這1毫秒內CPU不會空閒,它會去執行另一個處於就緒狀態的執行緒,這樣就可以讓子執行緒立即開始執行

3 執行(Running)狀態

當CPU開始排程處於 就緒狀態 的執行緒時,此時執行緒獲得了CPU時間片才得以真正開始執行run()方法的執行緒執行體,則該執行緒處於 執行狀態

  1. 如果計算機只有一個CPU,那麼在任何時刻只有一個執行緒處於執行狀態;
  2. 如果在一個多處理器的機器上,將會有多個執行緒並行執行,處於執行狀態;
  3. 當執行緒數大於處理器數時,依然會存在多個執行緒在同一個CPU上輪換的現象;

處於執行狀態的執行緒最為複雜,它 不可能一直處於執行狀態(除非它的執行緒執行體足夠短,瞬間就執行結束了),執行緒在執行過程中需要被中斷,目的是使其他執行緒獲得執行的機會,執行緒排程的細節取決於底層平臺所採用的策略。執行緒狀態可能會變為 阻塞狀態、就緒狀態和死亡狀態。比如:

  1. 對於採用 搶佔式策略 的系統而言,系統會給每個可執行的執行緒分配一個時間片來處理任務;當該時間片用完後,系統就會剝奪該執行緒所佔用的資源,讓其他執行緒獲得執行的機會。執行緒就會又 從執行狀態變為就緒狀態,重新等待系統分配資源;
  2. 對於採用 協作式策略的系統而言,只有當一個執行緒呼叫了它的yield()方法後才會放棄所佔用的資源—也就是必須由該執行緒主動放棄所佔用的資源,執行緒就會又 從執行狀態變為就緒狀態

4 阻塞(Blocked)狀態

處於執行狀態的執行緒在某些情況下,讓出CPU並暫時停止自己的執行,進入 阻塞狀態

當發生如下情況時,執行緒將會進入阻塞狀態:

  1. 執行緒呼叫sleep()方法,主動放棄所佔用的處理器資源,暫時進入中斷狀態(不會釋放持有的物件鎖),時間到後等待系統分配CPU繼續執行;
  2. 執行緒呼叫一個阻塞式IO方法,在該方法返回之前,該執行緒被阻塞;
  3. 執行緒試圖獲得一個同步監視器,但該同步監視器正被其他執行緒所持有;
  4. 程式呼叫了執行緒的suspend方法將執行緒掛起
  5. 執行緒呼叫wait,等待notify/notifyAll喚醒時(會釋放持有的物件鎖);

阻塞狀態分類:

  1. 等待阻塞:執行狀態中的 執行緒執行wait()方法,使本執行緒進入到等待阻塞狀態;
  2. 同步阻塞:執行緒在 獲取synchronized同步鎖失敗(因為鎖被其它執行緒佔用),它會進入到同步阻塞狀態;
  3. 其他阻塞:通過呼叫執行緒的 sleep()或join()或發出I/O請求 時,執行緒會進入到阻塞狀態。當 sleep()狀態超時、join()等待執行緒終止或者超時、或者I/O處理完畢 時,執行緒重新轉入就緒狀態;

在阻塞狀態的執行緒只能進入就緒狀態,無法直接進入執行狀態。而就緒和執行狀態之間的轉換通常不受程式控制,而是由系統執行緒排程所決定。當處於就緒狀態的執行緒獲得處理器資源時,該執行緒進入執行狀態;當處於執行狀態的執行緒失去處理器資源時,該執行緒進入就緒狀態

但有一個方法例外,呼叫yield()方法可以讓執行狀態的執行緒轉入就緒狀態

4.1 等待(WAITING)狀態

執行緒處於 無限制等待狀態,等待一個特殊的事件來重新喚醒,如:

  1. 通過wait()方法進行等待的執行緒等待一個notify()或者notifyAll()方法;
  2. 通過join()方法進行等待的執行緒等待目標執行緒執行結束而喚醒;

以上兩種一旦通過相關事件喚醒執行緒,執行緒就進入了 就緒(RUNNABLE)狀態 繼續執行。

4.2 時限等待(TIMED_WAITING)狀態

執行緒進入了一個 時限等待狀態,如:

sleep(3000),等待3秒後執行緒重新進行 就緒(RUNNABLE)狀態 繼續執行。

5 死亡(Dead)狀態

執行緒會以如下3種方式結束,結束後就處於 死亡狀態

  1. run()或call()方法執行完成,執行緒正常結束;
  2. 執行緒丟擲一個未捕獲的Exception或Error
  3. 直接呼叫該執行緒stop()方法來結束該執行緒—該方法容易導致死鎖,通常不推薦使用;

處於死亡狀態的執行緒物件也許是活的,但是,它已經不是一個單獨執行的執行緒。執行緒一旦死亡,就不能復生。 如果在一個死去的執行緒上呼叫start()方法,會丟擲java.lang.IllegalThreadStateException異常

所以,需要注意的是:

一旦執行緒通過start()方法啟動後就再也不能回到新建(NEW)狀態,執行緒終止後也不能再回到就緒(RUNNABLE)狀態

5.1 終止(TERMINATED)狀態

執行緒執行完畢後,進入終止(TERMINATED)狀態。

6 執行緒相關方法

public class Thread{
    // 執行緒的啟動
    public void start(); 
    // 執行緒體
    public void run(); 
    // 已廢棄
    public void stop(); 
    // 已廢棄
    public void resume(); 
    // 已廢棄
    public void suspend(); 
    // 在指定的毫秒數內讓當前正在執行的執行緒休眠
    public static void sleep(long millis); 
    // 同上,增加了納秒引數
    public static void sleep(long millis, int nanos); 
    // 測試執行緒是否處於活動狀態
    public boolean isAlive(); 
    // 中斷執行緒
    public void interrupt(); 
    // 測試執行緒是否已經中斷
    public boolean isInterrupted(); 
    // 測試當前執行緒是否已經中斷
    public static boolean interrupted(); 
    // 等待該執行緒終止
    public void join() throws InterruptedException; 
    // 等待該執行緒終止的時間最長為 millis 毫秒
    public void join(long millis) throws InterruptedException; 
    // 等待該執行緒終止的時間最長為 millis 毫秒 + nanos 納秒
    public void join(long millis, int nanos) throws InterruptedException; 
}
複製程式碼

執行緒方法狀態轉換

6.1 執行緒就緒、執行和死亡狀態轉換

  1. 就緒狀態轉換為執行狀態:此執行緒得到CPU資源;
  2. 執行狀態轉換為就緒狀態:此執行緒主動呼叫yield()方法或在執行過程中失去CPU資源。
  3. 執行狀態轉換為死亡狀態:此執行緒執行執行完畢或者發生了異常;

注意:

當呼叫執行緒中的yield()方法時,執行緒從執行狀態轉換為就緒狀態,但接下來CPU排程就緒狀態中的那個執行緒具有一定的隨機性,因此,可能會出現A執行緒呼叫了yield()方法後,接下來CPU仍然排程了A執行緒的情況。

6.2 run & start

通過呼叫start啟動執行緒,執行緒執行時會執行run方法中的程式碼。

  1. start():執行緒的啟動;
  2. run():執行緒的執行體;

6.3 sleep & yield

sleep():通過sleep(millis)使執行緒進入休眠一段時間,該方法在指定的時間內無法被喚醒,同時也不會釋放物件鎖

比如,我們想要使主執行緒每休眠100毫秒,然後再列印出數字:

/**
 * 可以明顯看到列印的數字在時間上有些許的間隔
 */
public class Test1 {  
    public static void main(String[] args) throws InterruptedException {  
        for(int i=0;i<100;i++){  
            System.out.println("main"+i);  
            Thread.sleep(100);  
        }  
    }  
} 
複製程式碼

注意如下幾點問題:

  1. sleep是靜態方法,最好不要用Thread的例項物件呼叫它因為它睡眠的始終是當前正在執行的執行緒,而不是呼叫它的執行緒物件它只對正在執行狀態的執行緒物件有效。看下面的例子:
    public class Test1 {  
        public static void main(String[] args) throws InterruptedException {  
            System.out.println(Thread.currentThread().getName());  
            MyThread myThread=new MyThread();  
            myThread.start();  
            // 這裡sleep的就是main執行緒,而非myThread執行緒 
            myThread.sleep(1000); 
            Thread.sleep(10);  
            for(int i=0;i<100;i++){  
                System.out.println("main"+i);  
            }  
        }  
    }  
    複製程式碼
  2. Java執行緒排程是Java多執行緒的核心,只有良好的排程,才能充分發揮系統的效能,提高程式的執行效率。但是不管程式設計師怎麼編寫排程,只能最大限度的影響執行緒執行的次序,而不能做到精準控制。因為使用sleep方法之後,執行緒是進入阻塞狀態的,只有當睡眠的時間結束,才會重新進入到就緒狀態,而就緒狀態進入到執行狀態,是由系統控制的,我們不可能精準的去幹涉它,所以如果呼叫Thread.sleep(1000)使得執行緒睡眠1秒,可能結果會大於1秒。
    public class Test1 {  
        public static void main(String[] args) throws InterruptedException {  
            new MyThread().start();  
            new MyThread().start();  
        }  
    }  
      
    class MyThread extends Thread {  
        @Override  
        public void run() {  
            for (int i = 0; i < 3; i++) {  
                System.out.println(this.getName()+"執行緒" + i + "次執行!");  
                try {  
                    Thread.sleep(50);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  
        }  
    } 
    複製程式碼
    看某一次的執行結果:可以發現,執行緒0首先執行,然後執行緒1執行一次,又了執行一次。發現並不是按照sleep的順序執行的。
    Thread-0執行緒0次執行!  
    Thread-1執行緒0次執行!  
    Thread-1執行緒1次執行!  
    Thread-0執行緒1次執行!  
    Thread-0執行緒2次執行!  
    Thread-1執行緒2次執行!  
    複製程式碼

yield():與sleep類似,也是Thread類提供的一個靜態的方法,它也可以讓當前正在執行的執行緒暫停,讓出CPU資源給其他的執行緒。但是和sleep()方法不同的是,它不會進入到阻塞狀態,而是進入到就緒狀態。yield()方法只是讓當前執行緒暫停一下,重新進入就緒執行緒池中,讓系統的執行緒排程器重新排程器重新排程一次,完全可能出現這樣的情況:當某個執行緒呼叫yield()方法之後,執行緒排程器又將其排程出來重新進入到執行狀態執行

實際上,當某個執行緒呼叫了yield()方法暫停之後,優先順序與當前執行緒相同,或者優先順序比當前執行緒更高的就緒狀態的執行緒更有可能獲得執行的機會,當然,只是有可能,因為我們不可能精確的干涉cpu排程執行緒。

public class Test1 {  
    public static void main(String[] args) throws InterruptedException {  
        new MyThread("低階", 1).start();  
        new MyThread("中級", 5).start();  
        new MyThread("高階", 10).start();  
    }  
}  
  
class MyThread extends Thread {  
    public MyThread(String name, int pro) {  
        super(name);// 設定執行緒的名稱  
        this.setPriority(pro);// 設定優先順序  
    }  
  
    @Override  
    public void run() {  
        for (int i = 0; i < 30; i++) {  
            System.out.println(this.getName() + "執行緒第" + i + "次執行!");  
            if (i % 5 == 0)  
                Thread.yield();  
        }  
    }  
}  
複製程式碼

關於sleep()方法和yield()方的區別如下

  1. sleep方法暫停當前執行緒後,會進入阻塞狀態,只有當睡眠時間到了,才會轉入就緒狀態。而yield方法呼叫後 ,是直接進入就緒狀態,所以有可能剛進入就緒狀態,又被排程到執行狀態;
  2. sleep方法宣告丟擲了InterruptedException,所以呼叫sleep方法的時候要捕獲該異常,或者顯示宣告丟擲該異常。而yield方法則沒有宣告丟擲任務異常
  3. sleep方法比yield方法有更好的可移植性,通常不要依靠yield方法來控制併發執行緒的執行

6.4 join

執行緒的合併的含義就是 將幾個並行執行緒的執行緒合併為一個單執行緒執行,應用場景是 當一個執行緒必須等待另一個執行緒執行完畢才能執行時,Thread類提供了join方法來完成這個功能,注意,它不是靜態方法

join有3個過載的方法:

void join()    
    當前執行緒等該加入該執行緒後面,等待該執行緒終止。    
void join(long millis)    
    當前執行緒等待該執行緒終止的時間最長為 millis 毫秒。 如果在millis時間內,該執行緒沒有執行完,那麼當前執行緒進入就緒狀態,重新等待cpu排程   
void join(long millis,int nanos)    
    等待該執行緒終止的時間最長為 millis 毫秒 + nanos 納秒。如果在millis時間內,該執行緒沒有執行完,那麼當前執行緒進入就緒狀態,重新等待cpu排程
複製程式碼

例子程式碼,如下

/**
 * 在主執行緒中呼叫thread.join(); 就是將主執行緒加入到thread子執行緒後面等待執行。不過有時間限制,為1毫秒。
 */
public class Test1 {  
    public static void main(String[] args) throws InterruptedException {  
        MyThread t=new MyThread();  
        t.start();  
        t.join(1);//將主執行緒加入到子執行緒後面,不過如果子執行緒在1毫秒時間內沒執行完,則主執行緒便不再等待它執行完,進入就緒狀態,等待cpu排程  
        for(int i=0;i<30;i++){  
            System.out.println(Thread.currentThread().getName() + "執行緒第" + i + "次執行!");  
        }  
    }  
}  
  
class MyThread extends Thread {  
    @Override  
    public void run() {  
        for (int i = 0; i < 1000; i++) {  
            System.out.println(this.getName() + "執行緒第" + i + "次執行!");  
        }  
    }  
}  
複製程式碼

在JDK中join方法的原始碼,如下:

public final synchronized void join(long millis)    throws InterruptedException {  
    long base = System.currentTimeMillis();  
    long now = 0;  
  
    if (millis < 0) {  
        throw new IllegalArgumentException("timeout value is negative");  
    }  
          
    if (millis == 0) {  
        while (isAlive()) {  
           wait(0);  
        }  
    } else {  
        while (isAlive()) {  
            long delay = millis - now;  
            if (delay <= 0) {  
                break;  
            }  
            wait(delay);  
            now = System.currentTimeMillis() - base;  
        }  
    }  
}  
複製程式碼

join方法實現是通過呼叫wait方法實現。當main執行緒呼叫t.join時候,main執行緒會獲得執行緒物件t的鎖(wait 意味著拿到該物件的鎖),呼叫該物件的wait(等待時間),直到該物件喚醒main執行緒,比如退出後。這就意味著main 執行緒呼叫t.join時,必須能夠拿到執行緒t物件的鎖

6.5 suspend & resume (已過時)

suspend-執行緒進入阻塞狀態,但不會釋放鎖。此方法已不推薦使用,因為同步時不會釋放鎖,會造成死鎖的問題

resume-使執行緒重新進入可執行狀態

為什麼 Thread.suspend 和 Thread.resume 被廢棄了?

Thread.suspend 天生容易引起死鎖。如果目標執行緒掛起時在保護系統關鍵資源的監視器上持有鎖,那麼其他執行緒在目標執行緒恢復之前都無法訪問這個資源。如果要恢復目標執行緒的執行緒在呼叫 resume 之前試圖鎖定這個監視器,死鎖就發生了。這種死鎖一般自身表現為“凍結( frozen )”程式。

其他相關資料:

  1. https://blog.csdn.net/dlite/article/details/4212915

6.6 stop(已過時)

不推薦使用,且以後可能去除,因為它不安全。為什麼 Thread.stop 被廢棄了?

因為其天生是不安全的。停止一個執行緒會導致其解鎖其上被鎖定的所有監視器(監視器以在棧頂產生ThreadDeath異常的方式被解鎖)。如果之前被這些監視器保護的任何物件處於不一致狀態,其它執行緒看到的這些物件就會處於不一致狀態。這種物件被稱為受損的 (damaged)。當執行緒在受損的物件上進行操作時,會導致任意行為。這種行為可能微妙且難以檢測,也可能會比較明顯。

不像其他未受檢的(unchecked)異常, ThreadDeath 悄無聲息的殺死及其他執行緒。因此,使用者得不到程式可能會崩潰的警告。崩潰會在真正破壞發生後的任意時刻顯現,甚至在數小時或數天之後。

其他相關資料:

  1. https://blog.csdn.net/dlite/article/details/4212915

6.7 wait & notify/notifyAll

wait & notify/notifyAll這三個都是Object類的方法。使用 wait ,notify 和 notifyAll 前提是先獲得呼叫物件的鎖

  1. 呼叫 wait 方法後,釋放持有的物件鎖,執行緒狀態有 Running 變為 Waiting,並將當前執行緒放置到物件的 等待佇列
  2. 呼叫notify 或者 notifyAll 方法後,等待執行緒依舊不會從 wait 返回,需要呼叫 noitfy 的執行緒釋放鎖之後,等待執行緒才有機會從 wait 返回
  3. notify 方法:將等待佇列的一個等待執行緒從等待佇列種移到同步佇列中 ,而 notifyAll 方法:將等待佇列種所有的執行緒全部移到同步佇列,被移動的執行緒狀態由 Waiting 變為 Blocked

前面一直提到兩個概念,等待佇列(等待池),同步佇列(鎖池),這兩者是不一樣的。具體如下:

同步佇列(鎖池):假設執行緒A已經擁有了某個物件(注意:不是類)的鎖,而其它的執行緒想要呼叫這個物件的某個synchronized方法(或者synchronized塊),由於這些執行緒在進入物件的synchronized方法之前必須先獲得該物件的鎖的擁有權,但是該物件的鎖目前正被執行緒A擁有,所以這些執行緒就進入了該物件的同步佇列(鎖池)中,這些執行緒狀態為Blocked

等待佇列(等待池):假設一個執行緒A呼叫了某個物件的wait()方法,執行緒A就會釋放該物件的鎖(因為wait()方法必須出現在synchronized中,這樣自然在執行wait()方法之前執行緒A就已經擁有了該物件的鎖),同時 執行緒A就進入到了該物件的等待佇列(等待池)中,此時執行緒A狀態為Waiting。如果另外的一個執行緒呼叫了相同物件的notifyAll()方法,那麼 處於該物件的等待池中的執行緒就會全部進入該物件的同步佇列(鎖池)中,準備爭奪鎖的擁有權。如果另外的一個執行緒呼叫了相同物件的notify()方法,那麼 僅僅有一個處於該物件的等待池中的執行緒(隨機)會進入該物件的同步佇列(鎖池)

被notify或notifyAll喚起的執行緒是有規律的,具體如下:

  1. 如果是通過notify來喚起的執行緒,那 先進入wait的執行緒會先被喚起來
  2. 如果是通過nootifyAll喚起的執行緒,預設情況是 最後進入的會先被喚起來,即LIFO的策略;

6.8 執行緒優先順序

每個執行緒執行時都有一個優先順序的屬性,優先順序高的執行緒可以獲得較多的執行機會,而優先順序低的執行緒則獲得較少的執行機會。與執行緒休眠類似,執行緒的優先順序仍然無法保障執行緒的執行次序。只不過,優先順序高的執行緒獲取CPU資源的概率較大,優先順序低的也並非沒機會執行

每個執行緒預設的優先順序都與建立它的父執行緒具有相同的優先順序,在預設情況下,main執行緒具有普通優先順序

Thread類提供了setPriority(int newPriority)和getPriority()方法來設定和返回一個指定執行緒的優先順序,其中setPriority方法的引數是一個整數,範圍是1~10之間,也可以使用Thread類提供的三個靜態常量:

MAX_PRIORITY   =10
MIN_PRIORITY   =1
NORM_PRIORITY   =5
複製程式碼

例子程式碼,如下

public class Test1 {  
    public static void main(String[] args) throws InterruptedException {  
        new MyThread("高階", 10).start();  
        new MyThread("低階", 1).start();  
    }  
}  
  
class MyThread extends Thread {  
    public MyThread(String name,int pro) {  
        super(name);//設定執行緒的名稱  
        setPriority(pro);//設定執行緒的優先順序  
    }  
    @Override  
    public void run() {  
        for (int i = 0; i < 100; i++) {  
            System.out.println(this.getName() + "執行緒第" + i + "次執行!");  
        }  
    }  
}  
複製程式碼

從執行結果可以看到 ,一般情況下,高階執行緒更顯執行完畢

注意一點

雖然Java提供了10個優先順序別,但這些優先順序別需要作業系統的支援。不同的作業系統的優先順序並不相同,而且也不能很好的和Java的10個優先順序別對應。所以我們應該使用MAX_PRIORITY、MIN_PRIORITY和NORM_PRIORITY三個靜態常量來設定優先順序,這樣才能保證程式最好的可移植性

6.9 守護執行緒

守護執行緒與普通執行緒寫法上基本沒啥區別,呼叫執行緒物件的方法setDaemon(true),則可以將其設定為守護執行緒。

守護執行緒使用的情況較少,但並非無用,舉例來說,JVM的垃圾回收、記憶體管理等執行緒都是守護執行緒。還有就是在做資料庫應用時候,使用的資料庫連線池,連線池本身也包含著很多後臺執行緒,監控連線個數、超時時間、狀態等等

setDaemon方法詳細說明

public final void setDaemon(boolean on):將該執行緒標記為守護執行緒或使用者執行緒。當正在執行的執行緒都是守護執行緒時,Java 虛擬機器退出

該方法必須在啟動執行緒前呼叫。 該方法首先呼叫該執行緒的 checkAccess 方法,且不帶任何引數。這可能丟擲 SecurityException(在當前執行緒中)。

引數:

on - 如果為 true,則將該執行緒標記為守護執行緒。
複製程式碼

丟擲:

 IllegalThreadStateException - 如果該執行緒處於活動狀態。
 SecurityException - 如果當前執行緒無法修改該執行緒。
複製程式碼
/** 
* Java執行緒:執行緒的排程-守護執行緒 
*/  
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次執行! 
複製程式碼

從上面的執行結果可以看出:前臺執行緒是保證執行完畢的,後臺執行緒還沒有執行完畢就退出了

實際上:JRE判斷程式是否執行結束的標準是所有的前臺執執行緒行完畢了,而不管後臺執行緒的狀態,因此,在使用後臺執行緒時候一定要注意這個問題

6.10 如何結束一個執行緒

Thread.stop()、Thread.suspend、Thread.resume、Runtime.runFinalizersOnExit 這些終止執行緒執行的方法已經被廢棄了,使用它們是極端不安全的!想要安全有效的結束一個執行緒,可以使用下面的方法。

  1. 正常執行完run方法,然後結束掉;
  2. 控制迴圈條件和判斷條件的識別符號來結束掉執行緒;

比如run方法這樣寫:只要保證在一定的情況下,run方法能夠執行完畢即可。而不是while(true)的無限迴圈。

class MyThread extends Thread {  
    int i=0;  
    @Override  
    public void run() {  
        while (true) {  
            if(i==10)  
                break;  
            i++;  
            System.out.println(i);  
              
        }  
    }  
}  
或者
class MyThread extends Thread {  
    int i=0;  
    boolean next=true;  
    @Override  
    public void run() {  
        while (next) {  
            if(i==10)  
                next=false;  
            i++;  
            System.out.println(i);  
        }  
    }  
}  
或者
class MyThread extends Thread {  
    int i=0;  
    @Override  
    public void run() {  
        while (true) {  
            if(i==10)  
                return;  
            i++;  
            System.out.println(i);  
        }  
    }  
}  
複製程式碼

誠然,使用上面方法的識別符號來結束一個執行緒,是一個不錯的方法,但其也有弊端,如果 該執行緒是處於sleep、wait、join的狀態時候,while迴圈就不會執行,那麼我們的識別符號就無用武之地了,當然也不能再通過它來結束處於這3種狀態的執行緒了

所以,此時可以使用interrupt這個巧妙的方式結束掉這個執行緒。我們先來看看sleep、wait、join方法的宣告:

public final void wait() throws InterruptedException 
public static native void sleep(long millis) throws InterruptedException
public final void join() throws InterruptedException
複製程式碼

可以看到,這三者有一個共同點,都丟擲了一個InterruptedException的異常。在什麼時候會產生這樣一個異常呢

每個Thread都有一箇中斷狀狀態,預設為false。可以通過Thread物件的isInterrupted()方法來判斷該執行緒的中斷狀態。可以通過Thread物件的interrupt()方法將中斷狀態設定為true。

當一個執行緒處於sleep、wait、join這三種狀態之一的時候,如果此時他的中斷狀態為true,那麼它就會丟擲一個InterruptedException的異常,並將中斷狀態重新設定為false。

看下面的簡單的例子:

public class Test1 {  
    public static void main(String[] args) throws InterruptedException {  
        MyThread thread=new MyThread();  
        thread.start();  
    }  
}  
  
class MyThread extends Thread {  
    int i=1;  
    @Override  
    public void run() {  
        while (true) {  
            System.out.println(i);  
            System.out.println(this.isInterrupted());  
            try {  
                System.out.println("我馬上去sleep了");  
                Thread.sleep(2000);  
                this.interrupt();  
            } catch (InterruptedException e) {  
                System.out.println("異常捕獲了"+this.isInterrupted());  
                return;  
            }  
            i++;  
        }  
    }  
}  
複製程式碼

測試結果:

1  
false  
我馬上去sleep了  
2  
true  
我馬上去sleep了  
異常捕獲了false 
複製程式碼

可以看到,首先執行第一次while迴圈,在第一次迴圈中,睡眠2秒,然後將中斷狀態設定為true。當進入到第二次迴圈的時候,中斷狀態就是第一次設定的true,當它再次進入sleep的時候,馬上就丟擲了InterruptedException異常,然後被我們捕獲了。然後中斷狀態又被重新自動設定為false了(從最後一條輸出可以看出來)。

所以,我們可以使用interrupt方法結束一個執行緒。具體使用如下:

public class Test1 {  
    public static void main(String[] args) throws InterruptedException {  
        MyThread thread=new MyThread();  
        thread.start();  
        Thread.sleep(3000);  
        thread.interrupt();  
    }  
}  
  
class MyThread extends Thread {  
    int i=0;  
    @Override  
    public void run() {  
        while (true) {  
            System.out.println(i);  
            try {  
                Thread.sleep(1000);  
            } catch (InterruptedException e) {  
                System.out.println("中斷異常被捕獲了");  
                return;  
            }  
            i++;  
        }  
    }  
} 
複製程式碼

多測試幾次,會發現一般有兩種執行結果:

0  
1  
2  
中斷異常被捕獲了
複製程式碼

或者

0  
1  
2  
3  
中斷異常被捕獲了 
複製程式碼

這兩種結果恰恰說明了,只要一個執行緒的中斷狀態一旦為true,只要它進入sleep等狀態,或者處於sleep狀態,立馬回丟擲InterruptedException異常

第一種情況,是當主執行緒從3秒睡眠狀態醒來之後,呼叫了子執行緒的interrupt方法,此時子執行緒正處於sleep狀態,立馬丟擲InterruptedException異常。

第二種情況,是當主執行緒從3秒睡眠狀態醒來之後,呼叫了子執行緒的interrupt方法,此時子執行緒還沒有處於sleep狀態。然後再第3次while迴圈的時候,在此進入sleep狀態,立馬丟擲InterruptedException異常。

相關文章