淺談 Java執行緒狀態轉換及控制

城北有個混子發表於2020-09-29

執行緒的狀態(系統層面)

  一個執行緒被建立後就進入了執行緒的生命週期。線上程的生命週期中,共包括新建(New)、就緒(Runnable)、執行(Running)、阻塞(Blocked)和死亡(Dead)這五種狀態。當執行緒啟動以後,CPU需要在多個執行緒之間切換,所以執行緒也會隨之在執行、阻塞、就緒這幾種狀態之間切換。

  執行緒的狀態轉換如圖:

  當使用new關鍵字建立一個執行緒物件後,該執行緒就處於新建狀態。此時的執行緒就是一個在堆中分配了記憶體的靜態的物件,執行緒的執行體(run方法的程式碼)不會被執行。

  當呼叫了執行緒物件的start()方法後,該執行緒就處於就緒狀態。此時該執行緒並沒有開始執行,而是處於可執行池中,Java虛擬機器會為該執行緒建立方法呼叫棧和程式計數器。至於該執行緒何時才能執行,要取決於JVM的排程。

  一旦處於就緒狀態的執行緒獲得CPU 開始執行,該執行緒就進入了執行狀態。執行緒執行時會執行run方法的程式碼。對於搶佔式策略的作業系統,系統會為每個可執行的執行緒分配一個時間片,當該時間片用盡後,系統會剝奪該執行緒所佔有的處理器資源,從而讓其他執行緒獲得佔有CPU 而執行的機會。此時該執行緒會從執行態轉為就緒態。

當一個正在執行的執行緒遇到如下情況時,執行緒會從執行態轉為阻塞態:

    ① 執行緒呼叫sleep、join等方法。

    ② 執行緒呼叫了一個阻塞式IO方法。

    ③ 執行緒試圖獲得一個同步監視器,但是該監視器正在被其他執行緒持有。

    ④ 執行緒在等待某個 notify 通知。

    ⑤ 程式呼叫了執行緒的suspend方法將該執行緒掛起。

  當執行緒被阻塞後,其他執行緒就有機會獲得CPU資源而被執行。當上述導致執行緒被阻塞的因素解除後,執行緒會回到就緒狀態等待處理機排程而被執行。

  當一個執行緒執行結束後,該執行緒進入死亡狀態。

有以下3種方式可結束一個執行緒:

  ① run 方法執行完畢。

  ② 執行緒丟擲一個異常或錯誤,而該異常或錯誤未被捕獲。

  ③ 呼叫執行緒的 stop方法結束該執行緒。(不推薦使用)

執行緒的控制

  Thread類中提供了一些控制執行緒的方法,通過這些方法可以輕鬆地控制一個執行緒的執行和執行狀態,以達到程式的預期效果。

join 方法

  如果執行緒A呼叫了執行緒B的join方法,執行緒A將被阻塞,等待執行緒B執行完畢後執行緒A才會被執行。這裡需要注意一點的是,join方法必須線上程B的start方法呼叫之後呼叫才有意義。join方法的主要作用就是實現執行緒間的同步,它可以使執行緒之間的並行執行變為序列執行。

join 方法有以下3種過載形式:

  ① join(): 等待被join的執行緒執行完成。

  ② join(long millis): 等待被join 的執行緒的時間為 millis 毫秒,如果該執行緒在millis 毫秒內未結束,則不再等待。

  ③ join(long millis,int nanos): 等待被join的執行緒的時間最長為 millis 毫秒加上nanos微秒。

public class JoinThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + "---" + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
}
public class TestThreadState {
    public static void main(String[] args) {
    
//      建立要加入當前執行緒的執行緒,並啟動
        JoinThread j1 = new JoinThread();
        j1.start();
        
//      加入當前執行緒,阻塞當前執行緒,直到加入執行緒執行完畢
        try {
            j1.join();
        } catch (InterruptedException e1) {
            // TODO Auto-generated catch block
            e1.printStackTrace();
        }
        
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName());
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
}

  我們定義了一個JoinThread類,它繼承了Thread類,這是我們要加入的執行緒類。

  在main方法中,我們建立了JoinThread執行緒,並把它加入到當前執行緒(主執行緒)中,並沒有指定當前執行緒等待的時間,所以會一直阻塞當前執行緒,直到JoinThread執行緒的run方法執行完畢,才會繼續執行當前執行緒。

sleep 方法

  當執行緒A呼叫了 sleep方法,則執行緒A將被阻塞,直到指定睡眠的時間到達後,執行緒A才會重新被喚起,進入就緒狀態。

sleep方法有以下2種過載形式:

  ① sleep(long millis):讓當前正在執行的執行緒暫停millis毫秒,該執行緒進入阻塞狀態。

  ② sleep(long mills,long nanos):讓當前正在執行的執行緒暫停 millis 毫秒加上 nanos微秒。

public class Test {

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "---" + i);
            try {
                Thread.sleep(1000);        // 阻塞當前執行緒1s
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

  這段程式碼中沒有建立其他執行緒,只有當前執行緒存在,也就是執行main函式的主執行緒。for迴圈中每列印一次執行緒名稱,主執行緒就會被sleep方法阻塞1s,然後進入就緒狀態,重新等待被調到,實現了執行緒的控制

yield 方法

  當執行緒A呼叫了yield方法,它可以暫時放棄處理器,但是執行緒A不會被阻塞,而是進入就緒狀態。

public class YieldThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "---" + i);
            // 主動放棄
            Thread.yield();
        }
    }
}

  我們自定義了一個執行緒類YieldThread,在run方法中定義了一個for迴圈,for迴圈中每列印一次執行緒名稱,就會呼叫一下yield方法,主動放棄CUP讓給其它有相同優先順序或更高優先順序的執行緒,自己進入就緒狀態,等待被CPU排程。

設定執行緒的優先順序

  每個執行緒都有自己的優先順序,預設情況下執行緒的優先順序都與建立該執行緒的父執行緒的優先順序相回。同時Thread類提供了setPriority(int priority) 和getPriority()方法設定和返回指定執行緒的優先順序。引數priority是一個整型資料,用以指定執行緒的優先順序。priority 的取值範圍是1-10,預設值為5,也可以使用Thread類提供的三個靜態常量設定執行緒的優先順序。

  ① MAX_PRIORITY:最高優先順序,其值為10。

  ② MIN_PRIORITY:最低優先順序,其值為1。

  ③ NORM_PRIORITY:普通優先順序,其值為5。

public class TestThreadPriority {
    public static void main(String[] args) {
        // 執行緒優先順序
        ThreadPriority p1 = new ThreadPriority();
        p1.setName("p1");
        ThreadPriority p2 = new ThreadPriority();
        p2.setName("p2");
        ThreadPriority p3 = new ThreadPriority();
        p3.setName("p3");
        
        p1.setPriority(1);
        p3.setPriority(10);
        
        p1.start();
        p2.start();
        p3.start();
    }
}

  我們建立了三個執行緒p1、p2、p3,設定了p1的優先順序為1,p3的優先順序為10,並沒有設定p2的,所以p2的優先順序預設是5。優先順序越高,表示獲取cup的機會越多,注意此處說的是機會,所以高優先順序的執行緒並不是一定先於低優先順序的執行緒被CPU排程,只是機會更大而已。

sleep方法和wait方法的區別是什麼?

  sleep 方法是Thread類的一個靜態方法,其作用是使執行中的執行緒暫時停止指定的毫秒數,從而該執行緒進入阻塞狀態並讓出處理器,將執行的機會讓給其他執行緒。但是這個過程中監控狀態始終保持,當sleep的時間到了之後執行緒會自動恢復。

  wait 方法是Object類的方法,它是用來實現執行緒同步的。當呼叫某個物件的wait方法後,當前執行緒會被阻塞並釋放同步鎖,直到其他執行緒呼叫了該物件的 notify 方法或者 notifyAll 方法來喚醒該執行緒。所以 wait 方法和 notify(或notifyAll)應當成對出現以保證執行緒間的協調執行。

sleep方法和yield方法的區別是什麼?

  ① sleep方法暫停當前執行緒後,會給其他執行緒執行機會而不會考慮其他執行緒的優先順序。但是yield方法只會給優先順序相同或者優先順序更高的執行緒執行機會。

  ② sleep方法執行後執行緒會進入阻塞狀態,而執行了yield方法後,當前執行緒會進入就緒狀態。

  ③ 由於sleep方法的宣告丟擲了 InterruptedException 異常,所以在呼叫sleep方法時需要catch 該異常或丟擲該異常,而yield 方法沒有宣告丟擲異常。

  ④ sleep 方法比yield 方法具有更好的可移植性。

補充一下sleep、yield、join和wait的差異:

  ① sleep、join、yield時並不釋放物件鎖資源,在wait操作時會釋放物件資源,wait在被notify/notifyAll喚醒時,重新去搶奪獲取物件鎖資源。

  ② sleep、join、yield可以在任何地方使用,而wait,notify,notifyAll只能在同步控制方法或者同步控制塊中使用。

  ③ 呼叫wait會立即釋放鎖,進入等待佇列,但是notify()不會立刻釋放sycronized(obj)中的物件鎖,必須要等notify()所線上程執行完sycronized(obj)同步塊中的所有程式碼才會釋放這把鎖,然後供等待的執行緒來搶奪物件鎖。

Java中為什麼不建議使用stop和suspend方法終止執行緒?

  在Java中可以使用stop 方法停止一個執行緒,使該執行緒進入死亡狀態。但是使用這種方法結束一個執行緒是不安全的,在編寫程式時應當禁止使用這種方法。

  之所以說stop方法是執行緒不安全的,是因為一旦呼叫了Thread.stop()方法,工作執行緒將丟擲一個ThreadDeath的異常,這會導致run方法結束執行,而且結束的點是不可控的,也就是說,它可能執行到run方法的任何一個位置就突然終止了。同時它還會釋放掉該執行緒所持有的鎖,這樣其他因為請求該鎖物件而被阻塞的執行緒就會獲得鎖物件而繼續執行下去。一般情況下,加鎖的目的是保護資料的一致性,然而如果在呼叫Thread.stop()後執行緒立即終止,那麼被保護資料就有可能出現不一致的情況(資料的狀態不可預知)。同時,該執行緒所持有的鎖突然被釋放,其他執行緒獲得同步鎖後可以進入臨界區使用這些被破壞的資料,這將有可能導致一些很奇怪的應用程式錯誤發生,而且這種錯誤非常難以debug.所以在這裡再次重申,不要試圖用stop 方法結束一個執行緒。

  suspend方法可以阻塞一個執行緒,然而該執行緒雖然被阻塞,但它仍然持有之前獲得的鎖,這樣其他任何執行緒都不能訪問相同鎖物件保護的資源,除非被阻塞的執行緒被重新恢復。如果此時只有一個執行緒能夠恢復這個被suspend的執行緒,但前提是先要訪問被該執行緒鎖定的臨界資源。這樣便產生了死鎖。所以在編寫程式時,應儘量避免使用suspend,如確實需要阻塞一個執行緒的執行,最好使用wait方法,這樣既可以阻塞掉當前正在執行的執行緒,同時又使得該執行緒不至於陷入死鎖。

  用一句話說就是:stop方法是執行緒不安全的,可能產生不可預料的結果;suspend方法可能導致死鎖。

如何終止一個執行緒?

  在Java 中不推薦使用stop方法和suspend方法終止一個執行緒,因為那是不安全的,那麼要怎樣終止一個執行緒呢?

方法一:使用退出標誌

  正常情況下,當Thread 或 Runnable 類的run方法執行完畢後該執行緒即可結束,但是有些情況下run方法可能永遠都不會停止,例如,在服務端程式中使用執行緒監聽客戶端請求,或者執行其他需要迴圈處理的任務。這時如果希望有機會終止該執行緒,可將執行的任務放在一個迴圈中(例如 while迴圈),並設定一個boolean型的迴圈結束的標誌。如果想使 while 迴圈在某一特定條件下退出,就可以通過設定這個標誌為true或false 來控制 while 迴圈是否退出。這樣將執行緒結束的控制邏輯與執行緒本身邏輯結合在一起,可以保證執行緒安全可控地結束。

  讓我們來看一看案例:

public class TestQuitSign {
    // 退出標誌
    public static volatile boolean quitFlag = false;
    
    // 退出標誌:針對執行時的執行緒
    public static void main(String[] args) {
        // 執行緒一:每隔一秒,列印一條資訊,當quitFlag為true時結束run方法。
        new Thread() {
            public void run() {
                System.out.println("thread start...");
                while (!quitFlag) {
                    try {
                        Thread.sleep(1000);
                    } catch (Exception e) {
                        
                    }
                    System.out.println("thread running...");
                }
                System.out.println("thread end...");
            }
        }.start();
        
        // 執行緒二:等待三秒,設定quitFlag為true,終止執行緒一。
        new Thread() {
            public void run() {
                try {
                    Thread.sleep(3000);
                } catch (Exception e) {
                    // TODO: handle exception
                }
                quitFlag = true;
            }
        }.start();
        
    }
}

  在上面這段程式中的main方法裡建立了兩個執行緒,第一個執行緒的run方法中有一個while迴圈,該迴圈通過boolean型變數quitFlag控制其是否結束。因為變數quitFlag的初始值為false,所以如果不修改該變數,第一個執行緒中的run方法將不會停止,也就是說,第一個執行緒將永遠不會終止,並且每隔1s在螢幕上列印出一條字串。第二個執行緒的作用是通過修改變數quitFlag來終止第一個執行緒。在第二個執行緒的run方法中首先將執行緒阻塞3s,然後將quitFlag置為true.因為變數quitFlag是同一程式中兩個執行緒共享的變數,所以可以通過修改quitFlag的值來控制第一個執行緒的執行。當變數quitFlag被置為true,第一個執行緒的while迴圈就可以終止,所以run方法就能執行完畢,從而安全退出第一個執行緒。

  注意,boolean 型變數 quitFlag 被宣告為 volatile,volatile 會保證變數在一個執行緒中的每一步操作在另一個執行緒中都是可見的,所以這樣可以確保將 quitFlag 置為true 後可以安全退出第一個執行緒。

方法二:使用 interrupt方法

  使用退出執行緒標誌的方法終止一個執行緒存在一定的侷限性,主要的限制就是這種方法只對執行中的執行緒起作用,如果該執行緒被阻塞(例如,呼叫了 Thread.join()方法或者Thread.sleep()方法等)而處於不可執行的狀態時,則退出執行緒標誌的方法將不會起作用。

  在這種情況下,可以使用Thread 提供的 interrupt()方法終止一個執行緒。因為該方法雖然不會中斷一個正在執行的執行緒,但是它可以使一個被阻塞的執行緒丟擲一箇中斷異常,從而使執行緒提前結束阻塞狀態,然後通過catch塊捕獲該異常,從而安全地結束該執行緒。

  我們來看看下面的例子:

public class TestInterrupt {

    // Interrupt方法: 針對阻塞狀態的執行緒
    public static void main(String[] args) throws InterruptedException{
        // 建立執行緒
        Thread thread = new Thread() {
            public void run() {
                System.out.println("thread start...");
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) { // 捕獲中斷異常
                    e.printStackTrace();
                }
                System.out.println("thread end...");
            }
        };
        // 啟動執行緒
        thread.start();
        
        // 主執行緒等待1秒,丟擲一箇中斷訊號
        Thread.sleep(1000);
        thread.interrupt();
    }
}

  在上面這段程式中的main方法裡建立了一個執行緒,在該執行緒的 run 方法中呼叫 sleep 函式將該執行緒阻塞10s.然後呼叫Thread 類的 start 方法啟動該執行緒,該執行緒剛剛被啟動就進入阻塞狀態。主執行緒等待1s後呼叫thread.interrupt()丟擲一箇中斷訊號,在run方法中的catch會正常捕獲到這個中斷訊號,這樣被阻塞的該執行緒就會提前退出阻塞狀態,不需要等待10s執行緒thread 就會被提前終止。

  上述方法主要針對當前執行緒呼叫了Thread.join()或者 Thread.sleep()等方法而被阻塞時終止該執行緒。如果一個執行緒被I/O阻塞,則無法通過thread.interrupt()丟擲一箇中斷訊號而離開阻塞狀態。這時可推而廣之,觸發一個與當前I/O0阻塞相關的異常,使其退出I/O阻塞,然後通過catch 塊捕獲該異常,從而安全地結束該執行緒。

總結一下:

  當一個執行緒處於執行狀態時,可通過設定退出標誌的方法安全結束該執行緒;當一個執行緒被阻塞而無法正常執行時,可以丟擲一個異常使其退出阻塞狀態,並 catch 住該異常從而安全結束該執行緒。

執行緒的狀態(JVM層面)

  我們在上面討論的執行緒狀態是從作業系統層面來看的,這樣看比較直觀,也容易理解,也是一個執行緒在作業系統中真實狀態的體現。下面我們來看看Java 中執行緒的狀態及轉換。

Java 執行緒狀態

在Java中執行緒的狀態有6種,我們來看一看JDK 1.8幫助文件中的說明:

JDK1.8幫助文件-執行緒狀態

  我們可以看到幫助文件中的最後一行,這些狀態是不反映任何作業系統執行緒狀態的JVM層面的狀態。我們來具體看一看這六種狀態:

NEW初始狀態,執行緒被建立,但是還沒有呼叫 start 方法。

RUNNABLED執行狀態,JAVA 執行緒把作業系統中的就緒和執行兩種狀態統稱為“執行狀態”。

BLOCKED阻塞狀態,表示執行緒進入等待狀態,也就是執行緒因為某種原因放棄了 CPU 使用權,阻塞也分為幾種情況 :

  • 等待阻塞:執行的執行緒執行了 Thread.sleep 、wait()、 join() 等方法JVM 會把當前執行緒設定為等待狀態,當 sleep 結束、join 執行緒終止或者wait執行緒被喚醒後,該執行緒從等待狀態進入到阻塞狀態,重新搶佔鎖後進行執行緒恢復;

  • 同步阻塞:執行的執行緒在獲取物件的同步鎖時,若該同步鎖被其他執行緒鎖佔用了,那麼jvm會把當前的執行緒放入到鎖池中 ;

  • 其他阻塞:發出了 I/O請求時,JVM 會把當前執行緒設定為阻塞狀態,當 I/O處理完畢則執行緒恢復;

WAITING等待狀態,沒有超時時間,要被其他執行緒喚醒或者有其它的中斷操作;

  • 執行 wait()
  • 執行 join()
  • 執行 LockSupport.park()

TIME_WAITING超時等待狀態,超時以後自動返回;

  • 執行 sleep(long)
  • 執行 wait(long)、join(long)
  • 執行 LockSupport.parkNanos(long)、LockSupport.parkUntil(long)

TERMINATED終止狀態,表示當前執行緒執行完畢 。

Java 執行緒狀態轉換

在這借用一下大佬的圖,因為這張圖畫真的很棒:

 總結一下

Java 執行緒的狀態:

作業系統層面:

  有5個狀態,分別是:New(新建)、Runnable(就緒)、Running(執行)、Blocked(阻塞)、Dead(死亡)。

JVM層面:

  有6個狀態,分別是:NEW(新建)、RUNNABLE(執行)、BLOCKED(阻塞)、WAITING(等待)、TIMED_WAITING(超時等待)、TERMINATED(終止)。

Java 執行緒的狀態控制:

  主要由這幾個方法來控制:sleep、join、yield、wait、notify以及notifyALL。

相關文章