Java面試必考題之執行緒的生命週期,結合原始碼,透徹講解!

JavaBuild發表於2024-03-10

寫在開頭

在前面的幾篇部落格裡,我們學習了Java的多執行緒,包括執行緒的作用、建立方式、重要性等,那麼今天我們就要正式踏入執行緒,去學習更加深層次的知識點了。

第一個需要學的就是執行緒的生命週期,也可以將之理解為執行緒的幾種狀態,以及互相之間的切換,這幾乎是Java多執行緒的面試必考題,每一年都有大量的同學,因為這部分內容回答不夠完美而錯過高薪,今天我們結合原始碼,好好來聊一聊。

執行緒的生命週期

所謂執行緒的生命週期,就是從它誕生到消亡的整個過程,而不同的程式語言,對執行緒生命週期的封裝是不同的,在Java中執行緒整個生命週期被分為了六種狀態,我們下面就來一起學習一下。

執行緒的6種狀態

對於Java中執行緒的狀態劃分,我們其實要從兩個方面去看,一是JVM層面,這是我們程式執行的核心,另一層面是作業系統層面,這是我們JVM能夠執行的核心。為了更直觀的分析,build哥列了一個對比圖:
image

在作業系統層面,對於RUNNABLE狀態拆分為(READY、RUNNING),那為什麼在JVM層面沒有分這麼細緻呢?

這是因為啊,在當下時分多工作業系統架構下,執行緒的驅動是透過獲取CPU時間片,而每個時間片的間隔非常之短(10-20ms),這就意味著一個執行緒在cpu上執行一次的時間在0.01秒,隨後CPU執行權就會發生切換,在如此高頻的切換下,JVM就沒必要去區分READY和RUNNING了。

在Java的原始碼中也可以看到,確實只分了6種狀態:

【原始碼解析1】

// Thread.State 原始碼
public enum State {
	//省略了每個列舉值上的註釋
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}

NEW(初始化狀態)

我們透過new一個Thread物件,進行了初始化工作,這時的執行緒還沒有被啟動。

【程式碼示例1】

public class Test {
    public static void main(String[] args) {
        //lambda 表示式
        Thread thread = new Thread(() -> {});
        System.out.println(thread.getState());
    }
}
//執行結果:NEW

我們透過thread.getState()方法去獲得當前執行緒所處在的狀態,此時輸出為NEW。

RUNNABLE(可執行狀態)

對於這種狀態的描述,我們來看一下Thread原始碼中如何說的:

/**
 * Thread state for a runnable thread. A thread in the runnable
 * state is executing in the Java virtual machine but it may
 * be waiting for other resources from the operating system
 * such as processor.
 */

大致意思是執行緒處於RUNNABLE狀態下,代表它可能正處於執行狀態,或者正在等待CPU資源的分配。

那麼我們怎樣從NEW狀態變為RUNNABLE呢?答案很簡單,我們只需要呼叫start()方法即可!

【程式碼示例2】

public class Test {
    public static void main(String[] args) {
        //lambda 表示式
        Thread thread = new Thread(() -> {});
        thread.start();
        System.out.println(thread.getState());
    }
}
//執行結果:RUNNABLE

BLOCKED(阻塞狀態)

當執行緒執行緒進入 synchronized 方法/塊或者呼叫 wait 後(被 notify)重新進入 synchronized 方法/塊,但是鎖被其它執行緒佔有,這個時候執行緒就會進入 BLOCKED(阻塞) 狀態。這時候只有等到鎖被另外一個執行緒釋放,重新獲取鎖後,阻塞狀態解除!

WAITING(無限時等待)

當透過程式碼將執行緒轉為WAITING狀態後,這種狀態不會自動切換為其他狀態,是一種無限時狀態,直到整個執行緒接收到了外界通知,去喚醒它,才會從WAITING轉為uRUNNABLE。
呼叫下面這 3 個方法會使執行緒進入等待狀態:

  1. Object.wait():使當前執行緒處於等待狀態直到另一個執行緒喚醒它;
  2. Thread.join():等待執行緒執行完畢,底層呼叫的是 Object 的 wait 方法;
  3. LockSupport.park():除非獲得呼叫許可,否則禁用當前執行緒進行執行緒排程。

TIMED_WAITING(有限時等待)

與WAITING相比,TIMED_WAITING是一種有限時的狀態,可以透過設定等待時間,沒有外界干擾的情況下,達到指定等待時間後,自動終止等待狀態,轉為RUNNABLE狀態。
呼叫如下方法會使執行緒進入超時等待狀態:

  1. Thread.sleep(long millis):使當前執行緒睡眠指定時間;
  2. Object.wait(long timeout):執行緒休眠指定時間,等待期間可以透過notify()/notifyAll()喚醒;
  3. Thread.join(long millis):等待當前執行緒最多執行 millis 毫秒,如果 millis 為 0,則會一直執行;
  4. LockSupport.parkNanos(long nanos): 除非獲得呼叫許可,否則禁用當前執行緒進行執行緒排程指定時間;
  5. LockSupport.parkUntil(long deadline):同上,也是禁止執行緒進行排程指定時間;

TERMINATED(終止狀態)

執行緒正常執行結束,或者異常終止,會轉變到 TERMINATED 狀態。

執行緒狀態的切換

上面的6種狀態隨著程式的執行,程式碼(方法)的執行,上下文的切換,也伴隨著狀態的轉變。
image

NEW 到 RUNNABLE 狀態

這一種轉變比較好理解,我們透過new,初始化一個Thread物件後,這時就是處於執行緒的NEW狀態,此時執行緒是不會獲取CPU時間片排程執行的,只有在呼叫了start()方法後,執行緒徹底建立完成,進入RUNNABLE狀態,等待作業系統排程執行!這種狀態是NEW -> RUNNABLE的單向轉變

RUNNABLE 與 BLOCKED 的狀態轉變

synchronized 修飾的方法、程式碼塊同一時刻只允許一個執行緒執行,其他執行緒只能等待,等待的執行緒會從 RUNNABLE 轉變到 BLOCKED 狀態,當等待的執行緒獲得 synchronized 隱式鎖時,就又會從 BLOCKED 轉變到 RUNNABLE 狀態。我們透過一段程式碼示例看一下:

【程式碼示例3】

public class Test {
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            testMethod();
        });
        Thread thread2 = new Thread(() -> {
            testMethod();
        });

        thread1.start();
        thread2.start();

        System.out.println(thread1.getName()+":"+thread1.getState());
        System.out.println(thread2.getName()+":"+thread2.getState());
    }
    // 同步方法爭奪鎖
    private static synchronized void testMethod() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

輸出:

Thread-0:RUNNABLE
Thread-1:BLOCKED

程式碼中在主執行緒中建立了2個執行緒,執行緒中都呼叫了同步方法,隨後去啟動執行緒,因為CPU的執行效率較高,還沒阻塞已經完成的列印,所以大部分時間裡會輸出兩執行緒均為RUNNABLE狀態;

當CPU效率稍低時,就會呈現上述結果,thread1啟動後進入RUNNABLE狀態,並且獲得了同步方法,這是thread2啟動後,呼叫的同步方法鎖已經被佔用,它作為等待的執行緒會從 RUNNABLE 轉變到 BLOCKED 狀態,待到thread1同步方法執行完畢,釋放synchronized鎖後,thread2獲得鎖,從BLOCKED轉為RUNNABLE狀態。

RUNNABLE 與 WAITING 的狀態轉變

1、獲得 synchronized 隱式鎖的執行緒,呼叫無引數的 Object.wait() 方法,狀態會從 RUNNABLE 轉變到 WAITING;呼叫 Object.notify()、Object.notifyAll() 方法,執行緒可能從 WAITING 轉變到 RUNNABLE 狀態。

2、呼叫無引數的 Thread.join() 方法。join() 是一種執行緒同步方法,如有一執行緒物件 Thread t,當呼叫 t.join() 的時候,執行程式碼的執行緒的狀態會從 RUNNABLE 轉變到 WAITING,等待 thread t 執行完。當執行緒 t 執行完,等待它的執行緒會從 WAITING 狀態轉變到 RUNNABLE 狀態。

3、呼叫 LockSupport.park() 方法,執行緒的狀態會從 RUNNABLE 轉變到 WAITING;呼叫 LockSupport.unpark(Thread thread) 可喚醒目標執行緒,目標執行緒的狀態又會從 WAITING 轉變為 RUNNABLE 狀態。

RUNNABLE 與 TIMED_WAITING 的狀態轉變

這種與上面的很相似,只是在方法呼叫和引數上有細微差別,因為,TIMED_WAITING 和 WAITING 狀態的區別,僅僅是呼叫的是超時引數的方法。
轉變方法在上文中已經提到了,這裡以sleep(time)為例,寫一個測試案例:

【程式碼示例4】

public class Test {
    public static void main(String[] args) throws InterruptedException {

        Thread thread1 = new Thread(() -> {
            testMethod();
        });
        Thread thread2 = new Thread(() -> {
           // testMethod();
        });
        thread1.start();
        Thread.sleep(1000L);
        thread2.start();

        System.out.println(thread1.getName()+":"+thread1.getState());
        System.out.println(thread2.getName()+":"+thread2.getState());
    }
    // 同步方法爭奪鎖
    private static synchronized void testMethod() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

輸出:

Thread-0:TIMED_WAITING
Thread-1:TERMINATED

這裡面我們啟動threa1後,讓主執行緒休眠了1秒,這時thread1獲得同步方法後,方法內部執行了休眠2秒的操作,因此它處於TIMED_WAITING狀態,而thread2正常執行結束,狀態處於TERMINATED(這個案例同樣可以印證下面RUNNABLE到TERMINATED的轉變)。

RUNNABLE 到 TERMINATED 狀態

轉變為TERMINATED狀態,表明這個執行緒已經執行完畢,通常用如下幾種情況:

  1. 執行緒執行完 run() 方法後,會自動轉變到 TERMINATED 狀態;
  2. 執行 run() 方法時異常丟擲,也會導致執行緒終止;
  3. Thread類的 stop() 方法已經不建議使用。

總結

今天關於執行緒的6種狀態就講到這裡啦,這是個重點知識點,希望大家能夠銘記於心呀!

結尾彩蛋

如果本篇部落格對您有一定的幫助,大家記得留言+點贊+收藏呀。原創不易,轉載請聯絡Build哥!

image

如果您想與Build哥的關係更近一步,還可以關注“JavaBuild888”,在這裡除了看到《Java成長計劃》系列博文,還有提升工作效率的小筆記、讀書心得、大廠面經、人生感悟等等,歡迎您的加入!

image

相關文章