啃碎併發(二):Java線程的生命週期

0 前言

當線程被建立並啓動之後,它既不是一啓動就進入了執行狀態,也不是一直處於執行狀態。在線程的生命週期中,它要通過 新建(New)、就緒(Runnable)、運行(Running)、阻塞(Blocked)和死亡(Dead)5種狀態。尤爲是當線程啓動之後,它不可能一直"霸佔"着CPU獨自運行,因此CPU須要在多條線程之間切換,因而 線程狀態也會屢次在運行、阻塞之間切換java

線程狀態轉換關係

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()方法以後當即執行而非"等待執行":bash

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

3 運行(Running)狀態

當CPU開始調度處於 就緒狀態 的線程時,此時線程得到了CPU時間片才得以真正開始執行run()方法的線程執行體,則該線程處於 運行狀態併發

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

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

  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異常。

相關文章
相關標籤/搜索