面試官:執行緒呼叫2次start會怎樣?我支支吾吾沒答上來

JavaBuild發表於2024-03-12

寫在開頭

在寫完上一篇文章《Java面試必考題之執行緒的生命週期,結合原始碼,透徹講解!》後,本以為這個小知識點就總結完了。

但剛剛吃晚飯時,突然想到了多年前自己面試時的親身經歷,決定再回來補充一個小知識點!

記得是一個週末去面試Java後端開發工程師崗位,面試官針對Java多執行緒進行了狂轟亂炸般的考問,什麼執行緒建立的方式、執行緒的狀態、各狀態間的切換、如果保證執行緒安全、各種鎖的區別,如何使用等等,因為有好好背八股文,所以七七八八的也答上來了,但最後面試官問了一個現在看來很簡單,但當時根本不知道的問題,他先是問了我,看過Thread的原始碼沒,我毫不猶豫的回答看過,緊接著他問:

執行緒在呼叫了一次start啟動後,再呼叫一次可以不?如果執行緒執行完,同樣再呼叫一次start又會怎麼樣?

這個問題拋給你們,請問該如何作答呢?

執行緒的啟動

我們知道雖然很多八股文面試題中說Java建立執行緒的方式有3種、4種,或者更多種,但實際上真正可以建立一個執行緒的只有new Thread().start();

【程式碼示例1】

public class Test {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {});
        System.out.println(thread.getName()+":"+thread.getState());
        thread.start();
        System.out.println(thread.getName()+":"+thread.getState());
    }
}

輸出:

Thread-0:NEW
Thread-0:RUNNABLE

建立一個Thread,這時執行緒處於NEW狀態,這時呼叫start()方法,會讓執行緒進入到RUNNABLE狀態。

RUNNABLE的執行緒呼叫start

在上面測試程式碼的基礎上,我們再次呼叫start()方法。

【程式碼示例2】

public class Test {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {});
        System.out.println(thread.getName()+":"+thread.getState());
        //第一次呼叫start
        thread.start();
        System.out.println(thread.getName()+":"+thread.getState());
        //第二次呼叫start
        thread.start();
        System.out.println(thread.getName()+":"+thread.getState());
    }
}

輸出:

Thread-0:NEW
Thread-0:RUNNABLE
Exception in thread "main" java.lang.IllegalThreadStateException
	at java.lang.Thread.start(Thread.java:708)
	at com.javabuild.server.pojo.Test.main(Test.java:17)

第二次呼叫時,程式碼丟擲IllegalThreadStateException異常。

這是為什麼呢?我們跟進start原始碼中一探究竟!

【原始碼解析1】

// 使用synchronized關鍵字保證這個方法是執行緒安全的
public synchronized void start() {
    // threadStatus != 0 表示這個執行緒已經被啟動過或已經結束了
    // 如果試圖再次啟動這個執行緒,就會丟擲IllegalThreadStateException異常
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    // 將這個執行緒新增到當前執行緒的執行緒組中
    group.add(this);

    // 宣告一個變數,用於記錄執行緒是否啟動成功
    boolean started = false;
    try {
        // 使用native方法啟動這個執行緒
        start0();
        // 如果沒有丟擲異常,那麼started被設為true,表示執行緒啟動成功
        started = true;
    } finally {
        // 在finally語句塊中,無論try語句塊中的程式碼是否丟擲異常,都會執行
        try {
            // 如果執行緒沒有啟動成功,就從執行緒組中移除這個執行緒
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            // 如果在移除執行緒的過程中發生了異常,我們選擇忽略這個異常
        }
    }
}

這裡有個threadStatus,若它不等於0表示執行緒已經啟動或結束,直接拋IllegalThreadStateException異常,我們在start原始碼中打上斷點,從第一次start中跟入進去,發現此時沒有報異常。
image

此時的threadStatus=0,執行緒狀態為NEW,斷點繼續向下走時,走到native方法start0()時,threadStatus=5,執行緒狀態為RUNNABLE。此時,我們從第二個start中進入斷點。
image

這時threadStatus=5,滿足不等於0條件,丟擲IllegalThreadStateException異常!

TERMINATED的執行緒呼叫start

終止狀態下的執行緒,情況和RUNNABLE類似!

【程式碼示例3】

public class Test {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {});
        thread.start();
        Thread.sleep(1000);
        System.out.println(thread.getName()+":"+thread.getState());
        thread.start();
        System.out.println(thread.getName()+":"+thread.getState());
    }
}

輸出:

Thread-0:TERMINATED
Exception in thread "main" java.lang.IllegalThreadStateException
	at java.lang.Thread.start(Thread.java:708)
	at com.javabuild.server.pojo.Test.main(Test.java:17)

這時同樣也滿足不等於0條件,丟擲IllegalThreadStateException異常!

我們其實可以跟入到state的原始碼中,看一看執行緒幾種狀態設定的邏輯。

【原始碼解析2】

// Thread.getState方法原始碼:
public State getState() {
    // get current thread state
    return sun.misc.VM.toThreadState(threadStatus);
}

// sun.misc.VM 原始碼:
// 如果執行緒的狀態值和4做位與操作結果不為0,執行緒處於RUNNABLE狀態。
// 如果執行緒的狀態值和1024做位與操作結果不為0,執行緒處於BLOCKED狀態。
// 如果執行緒的狀態值和16做位與操作結果不為0,執行緒處於WAITING狀態。
// 如果執行緒的狀態值和32做位與操作結果不為0,執行緒處於TIMED_WAITING狀態。
// 如果執行緒的狀態值和2做位與操作結果不為0,執行緒處於TERMINATED狀態。
// 最後,如果執行緒的狀態值和1做位與操作結果為0,執行緒處於NEW狀態,否則執行緒處於RUNNABLE狀態。
public static State toThreadState(int var0) {
    if ((var0 & 4) != 0) {
        return State.RUNNABLE;
    } else if ((var0 & 1024) != 0) {
        return State.BLOCKED;
    } else if ((var0 & 16) != 0) {
        return State.WAITING;
    } else if ((var0 & 32) != 0) {
        return State.TIMED_WAITING;
    } else if ((var0 & 2) != 0) {
        return State.TERMINATED;
    } else {
        return (var0 & 1) == 0 ? State.NEW : State.RUNNABLE;
    }
}

總結

OK,今天就講這麼多啦,其實現在回頭看看,這僅是一個簡單且微小的細節而已,但對於剛準備步入職場的我來說,卻是一個難題,今天寫出來,除了和大家分享一下Java執行緒中的小細節外,更多的是希望正在準備面試的小夥伴們,能夠心細,多看原始碼,多問自己為什麼?並去追尋答案,Java開發不可淺嘗輒止。

結尾彩蛋

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

image

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

image

相關文章