Java—執行緒的生命週期及執行緒控制方法詳解

Andya_net發表於2020-05-19

執行緒生命週期5種狀態

介紹

  執行緒的生命週期經過新建(New)、就緒(Runnable)、執行(Running)、阻塞(Bolocked)和死亡(Dead)

狀態轉換圖

在這裡插入圖片描述

新建(New)

  程式使用new關鍵字建立一個執行緒之後,該執行緒就處於新建狀態,僅僅由Java虛擬機器為其分配記憶體,並初始化其成員變數的值不會執行執行緒的執行緒執行體。如Thread thread = new Thread()

就緒(Runnable)

  也稱為“可執行狀態”,執行緒物件呼叫start()方法後,該執行緒處於就緒狀態。如thread.start()。Java虛擬機器會為其建立方法呼叫棧和程式計數器(執行緒私有),處於就緒狀態的執行緒並沒有開始執行,只是表示該執行緒可以執行,執行緒何時執行取決於JVM中執行緒排程器的排程。

執行(Running)

  處於就緒狀態的執行緒獲得CPU,開始執行run()方法的執行緒執行體,則該執行緒處於執行狀態。(注意:執行緒只能從就緒狀態進入到執行狀態)

阻塞(Boloked)

  阻塞狀態是執行緒因為某種原因放棄了CPU的使用權,暫時停止執行,直到執行緒進入就緒狀態,才有機會轉到執行狀態。當呼叫sleep()、一個阻塞式IO方法、同步鎖、等待通知、suspend()方法掛起都會使執行緒進入阻塞狀態。

  • 執行緒呼叫sleep()方法主動放棄所佔用的處理器資源;
  • 執行緒呼叫一個阻塞式IO方法,在該方法返回之前,該執行緒被阻塞;
  • 執行緒試圖獲得一個同步監視器,但該同步監視器正被其他執行緒所持有;
  • 執行緒在等待(wait())某個通知(notify());
  • 程式呼叫了執行緒的suspend()方法將該執行緒掛起,但這個方法易造成死鎖,應該避免使用。

  執行緒從阻塞狀態解除——進入就緒狀態的過程:

  • 呼叫sleep()方法的執行緒經過了指定時間
  • 執行緒呼叫的阻塞式IO方法已經返回
  • 執行緒成功地獲得試圖取得的同步監視器(鎖)
  • 執行緒正在等待某個通知時,其他執行緒發出了一個通知
  • 處於掛起狀態的執行緒被呼叫了resume()恢復方法。

死亡(Dead)

以如下3種方式結束執行緒

  • run()call()方法執行完成,執行緒正常結束;
  • 執行緒丟擲一個未捕獲的ExceptionError
  • 直接呼叫該執行緒的stop()方法來結束該執行緒(該方法易造成死鎖,不推薦使用)

注意:

  • 當丟擲一個異常後程式會結束,所以執行緒會終止;
  • sleep()方法會阻塞一個執行緒並不會終止;
  • 建立一個新的執行緒也不會終止另一個執行緒。

判斷執行緒是否死亡

  可以通過isAlive()方法,執行緒物件的isAlive()方法返回true,即為執行緒存活;返回false,即為執行緒死亡。
  執行緒處於就緒、執行、阻塞狀態時,isAlive()返回true;執行緒處於新建、死亡狀態時,isAlive()返回false

start()和run()方法詳解

start()和run()介紹

  當程式使用new關鍵字建立了一個執行緒後,該執行緒就處於新建狀態,此時它和其他Java物件是一樣的,只是由JVM為其分配記憶體,並初始化其成員變數的值(此時執行緒物件沒有任何的行為,也不執行執行緒執行體)。
  當執行緒物件呼叫了start()方法後,執行緒就處於就緒狀態,JVM為其建立方法呼叫棧和程式計數器,處於這個狀態中的執行緒還沒有真正的開始執行,只是表示這個執行緒此時是一個可執行狀態。何時能執行?取決於JVM的執行緒排程器的排程。
  處於就緒狀態的執行緒獲取CPU執行許可權,開始執行run()方法的執行緒執行體,此時執行緒處於執行狀態。(若只有一個CPU,任何時刻只能有一個執行緒處於執行狀態,多執行緒處理任務時,會給人一種併發錯覺,實際是CPU執行速度較快,多執行緒交織執行任務而已)

start()方法原始碼

 public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        //若執行緒不是就緒狀態,就丟擲異常
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        //將執行緒新增到ThreadGroup中
        group.add(this);

        boolean started = false;
        try {
        	//通過start0()方法啟動執行緒
            start0();
            //設定執行緒啟動的started標誌位
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

  start()實際上通過本地方法start0()啟動執行緒,會新執行一個執行緒,新執行緒會呼叫run()方法。

run()方法原始碼

    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

  targetRunnable物件run()直接呼叫Thread執行緒Runnable成員run()方法,並不會新建一個執行緒。

執行緒控制方法

sleep()方法

sleep()方法介紹

  1. sleep(long millis)方法是Thread類的一個靜態方法,作用是讓當前執行緒暫停一段時間,並進入阻塞狀態。

sleep()方法過載方式

  • public static native void sleep(long millis) throws InterruptedException:讓當前正在執行的執行緒暫停millis毫秒,並進入阻塞狀態。
  • public static void sleep(long millis, int nanos) throws InterruptedException:讓當前正在執行的執行緒暫停millis毫秒+nanos毫微秒,並進入阻塞狀態。(很少用)

sleep()示例

通常用法就是

//讓當前執行緒睡眠1000毫秒,即暫定1s
Thread.sleep(1000);

yield()方法

yield()方法介紹

  1. yield()方法讓當前正在執行的執行緒暫停,但不會阻塞執行緒,只是讓執行緒轉入就緒狀態。
  2. yield()方法讓當前執行緒暫停,讓系統的執行緒排程重新排程一次,所以會出現當某個執行緒呼叫了yield()方法後,執行緒排程器又重新將它排程出來執行。
  3. yield()方法讓當前執行緒暫停後,只有優先順序>=當前執行緒的處於就緒狀態的執行緒才能獲取CPU執行許可權。

yield()方法過載

  • public static native void yield();:靜態方法。

yield()示例

//讓當前執行緒暫停
Thread.yield();

執行緒優先順序

  1. 每個執行緒執行都有一定的優先順序,優先順序高的執行緒獲得CPU執行許可權的機會比較大。
  2. 每個執行緒預設的優先順序與建立它的父執行緒的優先順序相同。所以main執行緒的優先順序一般和自己建立的子執行緒優先順序一樣。
  3. Thread類提供setPriority(int newPriority)getPriority()方法設定和返回指定執行緒的優先順序。其中setPriority()方法的引數可以是一個整數(1-10之間),也可以是靜態常量。
    MAX_PRIORITY:值為10.
    MIN_PRIORITY:值為1.
    NORM_PRIORITY:值為5.

join()方法

join()方法介紹

  1. Thread類提供join()方法讓一個執行緒等待另一個執行緒完成的方法;就是將指定的執行緒加入到當前執行緒,這樣兩個交替執行的執行緒就變成了順序執行的執行緒,如執行緒A呼叫了執行緒B的join()方法,則執行緒A會等待執行緒B執行完畢後才會繼續執行自己。
  2. join()方法由使用執行緒的程式呼叫,呼叫執行緒呼叫執行緒t的t.join()方法後將會被阻塞,直到執行緒t執行完畢,呼叫執行緒才能繼續執行。一般就是用於主執行緒內,等待其他執行緒執行完畢後,再繼續執行main()函式。

join()方法過載方式

  • public final void join() throws InterruptedException:等待被join的執行緒執行完畢。
  • public final synchronized void join(long millis) throws InterruptedException:等待被join的執行緒的超時時間為millis毫秒。如果在millis毫秒內被join的執行緒還未結束執行流程,則呼叫執行緒不再等待。
  • public final synchronized void join(long millis, int nanos) throws InterruptedException:等待被join的執行緒的時間最長為millis毫秒+nanos毫微秒。(很少用)

join()方法示例

(1)未使用join()方法

程式碼

public class JoinMethodTest {

    public static void main(String[] args) {
        System.out.println("main thread start");

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("child thread start");
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println("child thread finshed");
            }
        });
        thread.start();
        System.out.println("main thread finshed");
    }
}

執行結果

main thread start
main thread finshed
child thread start
child thread finshed

  可以從執行結果看出,main()主執行緒日誌列印的很快,沒有等待子執行緒列印就結束了。

(2)使用join()方法

程式碼

public class JoinMethodTest {

    public static void main(String[] args) {
        System.out.println("main thread start");

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("child thread start");
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println("child thread finshed");
            }
        });
        thread.start();
        //加入join()方法等待子執行緒執行完畢,才執行主執行緒。
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("main thread finshed");
    }
}

執行結果

main thread start
child thread start
child thread finshed
main thread finshed

  從執行結果可以看出,main thread finshed結果是在最後列印的,加入join()方法等待子執行緒執行完畢,才執行主執行緒。

6種狀態的執行緒生命週期解釋

在這裡插入圖片描述

Q&A

為何啟動執行緒需要用start()方法而不是直接呼叫run()方法?

  1. 呼叫start()方法啟動執行緒,系統會將該執行緒物件的run()方法當作執行緒執行體來處理。
  2. 直接呼叫執行緒物件的run()方法,該方法會被立即執行,而在run()方法返回之前其他執行緒無法併發執行(系統會將執行緒物件的當作一個普通物件,將run()方法當作一個普通方法,而不是執行緒執行體。)

start()方法和run()方法

java Thread中,run方法和start()方法的區別
  • 概念start()是啟動執行緒,讓執行緒從新建狀態變為就緒狀態;執行緒得到CPU時間片後,執行run()中的執行緒執行體;
  • 呼叫次數start()只能呼叫一次;run()可以重複呼叫。
  • 方法型別:啟動執行緒只能用start(),系統會把run()方法當做執行緒執行體處理;如果直接呼叫run(),系統會把執行緒物件當作普通物件,此時run()也是一個普通方法,而不是執行緒執行體。run()方法只是類的一個普通方法而已,如果直接呼叫run方法,程式中依然只有主執行緒這一個執行緒,其程式執行路徑還是隻有一條,還是要順序執行,還是要等待run方法體執行完畢後才可繼續執行下面的程式碼。。
  • 原始碼start()原始碼中實際上通過本地方法start0()啟動執行緒,會新執行一個執行緒,新執行緒會呼叫run()方法;而run()原始碼中targetRunnable物件run()直接呼叫Thread執行緒Runnable成員的run()方法,並不會新建一個執行緒。
  • 多執行緒:用 start方法來啟動執行緒,是真正實現了多執行緒, 通過呼叫Thread類的start()方法來啟動一個執行緒,這時此執行緒處於就緒(可執行)狀態,並沒有執行,一旦得到cpu時間片,就開始執行run()方法。但要注意的是,此時無需等待run()方法執行完畢,即可繼續執行下面的程式碼。所以run()方法並沒有實現多執行緒。

sleep()和yield()方法的區別

  1. 依賴執行緒優先順序:sleep()方法暫停當前執行緒後,會給其他執行緒執行機會,而不在乎其他執行緒的優先順序;
    yield()方法暫停當前執行緒後,只會給優先順序相同或更高的執行緒執行機會。
  2. 執行緒轉入狀態:sleep()方法將執行緒轉入阻塞狀態,知道經過阻塞時間才會轉入就緒狀態;
    yield()方法不會將執行緒轉入阻塞狀態,而是將執行緒轉入就緒狀態。
  3. 異常宣告:sleep()方法宣告丟擲了InterruptedException異常;
    yield()方法未宣告丟擲異常。
  4. 可移植性: sleep()方法的移植性比yield()方法好,所以一般使用sleep()方法控制併發程式設計。

相關文章