淺談 Java線程狀態轉換及控制


線程的狀態(系統層面)

  一個線程被創建后就進入了線程的生命周期。在線程的生命周期中,共包括新建(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。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM