Java執行緒生命週期與狀態切換

throwable發表於2020-08-05

前提

最近有點懶散,沒什麼比較有深度的產出。剛好想重新研讀一下JUC執行緒池的原始碼實現,在此之前先深入瞭解一下Java中的執行緒實現,包括執行緒的生命週期、狀態切換以及執行緒的上下文切換等等。編寫本文的時候,使用的JDK版本是11。

Java執行緒的實現

JDK1.2之後,Java執行緒模型已經確定了基於作業系統原生執行緒模型實現。因此,目前或者今後的JDK版本中,作業系統支援怎麼樣的執行緒模型,在很大程度上決定了Java虛擬機器的執行緒如何對映,這一點在不同的平臺上沒有辦法達成一致,虛擬機器規範中也未限定Java執行緒需要使用哪種執行緒模型來實現。執行緒模型只對執行緒的併發規模和操作成本產生影響,對於Java程式來說,這些差異是透明的。

對應Oracle Sun JDK或者說Oracle Sun JVM而言,它的Windows版本和Linux版本都是使用一對一的執行緒模型實現的(如下圖所示)。

j-t-l-s-1.png

也就是一條Java執行緒就對映到一條輕量級程式(Light Weight Process)中,而一條輕量級執行緒又對映到一條核心執行緒(Kernel-Level Thread)。我們平時所說的執行緒,往往就是指輕量級程式(或者通俗來說我們平時新建的java.lang.Thread就是輕量級程式例項的一個"控制程式碼",因為一個java.lang.Thread例項會對應JVM裡面的一個JavaThread例項,而JVM裡面的JavaThread就應該理解為輕量級程式)。前面推算這個執行緒對映關係,可以知道,我們在應用程式中建立或者操作的java.lang.Thread例項最終會對映到系統的核心執行緒,如果我們惡意或者實驗性無限建立java.lang.Thread例項,最終會影響系統的正常執行甚至導致系統崩潰(可以在Windows開發環境中做實驗,確保記憶體足夠的情況下使用死迴圈建立和執行java.lang.Thread例項)。

執行緒排程方式包括兩種,協同式執行緒排程和搶佔式執行緒排程。

執行緒排程方式 描述 劣勢 優勢
協同式執行緒排程 執行緒的執行時間由執行緒本身控制,執行完畢後主動通知作業系統切換到另一個執行緒上 某個執行緒如果不讓出CPU執行時間可能會導致整個系統崩潰 實現簡單,沒有執行緒同步的問題
搶佔式執行緒排程 每個執行緒由作業系統來分配執行時間,執行緒的切換不由執行緒自身決定 實現相對複雜,作業系統需要控制執行緒同步和切換 不會出現一個執行緒阻塞導致系統崩潰的問題

Java執行緒最終會對映為系統核心原生執行緒,所以Java執行緒排程最終取決於系作業系統,而目前主流的作業系統核心執行緒排程基本都是使用搶佔式執行緒排程。也就是可以死記硬背一下:Java執行緒是使用搶佔式執行緒排程方式進行執行緒排程的

很多作業系統都提供執行緒優先順序的概念,但是由於平臺特性的問題,Java中的執行緒優先順序和不同平臺中系統執行緒優先順序並不匹配,所以Java執行緒優先順序可以僅僅理解為“建議優先順序”,通俗來說就是java.lang.Thread#setPriority(int newPriority)並不一定生效,有可能Java執行緒的優先順序會被系統自行改變

Java執行緒的狀態切換

Java執行緒的狀態可以從java.lang.Thread的內部列舉類java.lang.Thread$State得知:

public enum State {
      
    NEW,

    RUNNABLE,

    BLOCKED,

    WAITING,

    TIMED_WAITING,

    TERMINATED;
}

這些狀態的描述總結成圖如下:

j-t-l-s-3

執行緒狀態之間關係切換圖如下:

j-t-l-s-2

下面通過API註釋和一些簡單的程式碼例子分析一下Java執行緒的狀態含義和狀態切換。

NEW狀態

API註釋

/**
 * Thread state for a thread which has not yet started.
 *
 */
NEW,

執行緒例項尚未啟動時候的執行緒狀態。

一個剛建立而尚未啟動(尚未呼叫Thread#start()方法)的Java執行緒例項的就是處於NEW狀態。

public class ThreadState {

    public static void main(String[] args) throws Exception {
        Thread thread = new Thread();
        System.out.println(thread.getState());
    }
}

// 輸出結果
NEW

RUNNABLE狀態

API註釋

/**
 * 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,

可執行狀態下執行緒的執行緒狀態。可執行狀態下的執行緒在Java虛擬機器中執行,但它可能執行等待作業系統的其他資源,例如處理器。

當Java執行緒例項呼叫了Thread#start()之後,就會進入RUNNABLE狀態。RUNNABLE狀態可以認為包含兩個子狀態:READYRUNNING

  • READY:該狀態的執行緒可以被執行緒排程器進行排程使之更變為RUNNING狀態。
  • RUNNING:該狀態表示執行緒正在執行,執行緒物件的run()方法中的程式碼所對應的的指令正在被CPU執行。

當Java執行緒例項Thread#yield()方法被呼叫時或者由於執行緒排程器的排程,執行緒例項的狀態有可能由RUNNING轉變為READY,但是從執行緒狀態Thread#getState()獲取到的狀態依然是RUNNABLE。例如:

public class ThreadState1 {

    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(()-> {
            while (true){
                Thread.yield();
            }
        });
        thread.start();
        Thread.sleep(2000);
        System.out.println(thread.getState());
    }
}
// 輸出結果
RUNNABLE

WAITING狀態

API註釋

   /**
    * Thread state for a waiting thread.
    * A thread is in the waiting state due to calling one of the
    * following methods:
    * <ul>
    *   <li>{@link Object#wait() Object.wait} with no timeout</li>
    *   <li>{@link #join() Thread.join} with no timeout</li>
    *   <li>{@link LockSupport#park() LockSupport.park}</li>
    * </ul>
    *
    * <p>A thread in the waiting state is waiting for another thread to
    * perform a particular action.
    *
    * For example, a thread that has called {@code Object.wait()}
    * on an object is waiting for another thread to call
    * {@code Object.notify()} or {@code Object.notifyAll()} on
    * that object. A thread that has called {@code Thread.join()}
    * is waiting for a specified thread to terminate.
    */
    WAITING,

等待中執行緒的狀態。一個執行緒進入等待狀態是由於呼叫了下面方法之一:
不帶超時的Object#wait()
不帶超時的Thread#join()
LockSupport.park()
一個處於等待狀態的執行緒總是在等待另一個執行緒進行一些特殊的處理。
例如:一個執行緒呼叫了Object#wait(),那麼它在等待另一個執行緒呼叫物件上的Object#notify()或者Object#notifyAll();一個執行緒呼叫了Thread#join(),那麼它在等待另一個執行緒終結。

WAITING無限期的等待狀態,這種狀態下的執行緒不會被分配CPU執行時間。當一個執行緒執行了某些方法之後就會進入無限期等待狀態,直到被顯式喚醒,被喚醒後,執行緒狀態由WAITING更變為RUNNABLE然後繼續執行。

RUNNABLE轉換為WAITING的方法(無限期等待) WAITING轉換為RUNNABLE的方法(喚醒)
Object#wait() Object#notify() | Object#notifyAll()
Thread#join() -
LockSupport.part() LockSupport.unpart(thread)

其中Thread#join()方法相對比較特殊,它會阻塞執行緒例項直到執行緒例項執行完畢,可以觀察它的原始碼如下:

public final void join() throws InterruptedException {
    join(0);
}

public final synchronized void join(long millis)throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

可見Thread#join()是線上程例項存活的時候總是呼叫Object#wait()方法,也就是必須線上程執行完畢isAlive()為false(意味著執行緒生命週期已經終結)的時候才會解除阻塞。

基於WAITING狀態舉個例子:

public class ThreadState3 {

    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(()-> {
            LockSupport.park();
            while (true){
                Thread.yield();
            }
        });
        thread.start();
        Thread.sleep(50);
        System.out.println(thread.getState());
        LockSupport.unpark(thread);
        Thread.sleep(50);
        System.out.println(thread.getState());
    }
}
// 輸出結果
WAITING
RUNNABLE

TIMED WAITING狀態

API註釋

/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <ul>
*   <li>{@link #sleep Thread.sleep}</li>
*   <li>{@link Object#wait(long) Object.wait} with timeout</li>
*   <li>{@link #join(long) Thread.join} with timeout</li>
*   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
*   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,

定義了具體等待時間的等待中執行緒的狀態。一個執行緒進入該狀態是由於指定了具體的超時期限呼叫了下面方法之一:
Thread.sleep()
帶超時的Object#wait()
帶超時的Thread#join()
LockSupport.parkNanos()
LockSupport.parkUntil()

TIMED WAITING就是有限期等待狀態,它和WAITING有點相似,這種狀態下的執行緒不會被分配CPU執行時間,不過這種狀態下的執行緒不需要被顯式喚醒,只需要等待超時限期到達就會被VM喚醒,有點類似於現實生活中的鬧鐘。

RUNNABLE轉換為TIMED WAITING的方法(有限期等待) TIMED WAITING轉換為RUNNABLE的方法(超時解除等待)
Object#wait(timeout) -
Thread#sleep(timeout) -
Thread#join(timeout) -
LockSupport.parkNanos(timeout) -
LockSupport.parkUntil(timeout) -

舉個例子:

public class ThreadState4 {

    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(()-> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                //ignore
            }
        });
        thread.start();
        Thread.sleep(50);
        System.out.println(thread.getState());
        Thread.sleep(1000);
        System.out.println(thread.getState());
    }
}
// 輸出結果
TIMED_WAITING
TERMINATED

BLOCKED狀態

API註釋

/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
BLOCKED,

此狀態表示一個執行緒正在阻塞等待獲取一個監視器鎖。如果執行緒處於阻塞狀態,說明執行緒等待進入同步程式碼塊或者同步方法的監視器鎖或者在呼叫了Object#wait()之後重入同步程式碼塊或者同步方法。

BLOCKED狀態也就是阻塞狀態,該狀態下的執行緒不會被分配CPU執行時間。執行緒的狀態為BLOCKED的時候有兩種可能的情況:

A thread in the blocked state is waiting for a monitor lock to enter a synchronized block/method

  1. 執行緒正在等待一個監視器鎖,只有獲取監視器鎖之後才能進入synchronized程式碼塊或者synchronized方法,在此等待獲取鎖的過程執行緒都處於阻塞狀態。

reenter a synchronized block/method after calling Object#wait()

  1. 執行緒X步入synchronized程式碼塊或者synchronized方法後(此時已經釋放監視器鎖)呼叫Object#wait()方法之後進行阻塞,當接收其他執行緒T呼叫該鎖物件Object#notify()/notifyAll(),但是執行緒T尚未退出它所在的synchronized程式碼塊或者synchronized方法,那麼執行緒X依然處於阻塞狀態(注意API註釋中的reenter,理解它場景2就豁然開朗)。

更加詳細的描述可以參考筆者之前寫過的一篇文章:深入理解Object提供的阻塞和喚醒API

針對上面的場景1舉個簡單的例子:

public class ThreadState6 {

    private static final Object MONITOR = new Object();

    public static void main(String[] args) throws Exception {
        Thread thread1 = new Thread(()-> {
            synchronized (MONITOR){
                try {
                    Thread.sleep(Integer.MAX_VALUE);
                } catch (InterruptedException e) {
                    //ignore
                }
            }
        });
        Thread thread2 = new Thread(()-> {
            synchronized (MONITOR){
                System.out.println("thread2 got monitor lock...");
            }
        });
        thread1.start();
        Thread.sleep(50);
        thread2.start();
        Thread.sleep(50);
        System.out.println(thread2.getState());
    }
}
// 輸出結果
BLOCKED

針對上面的場景2舉個簡單的例子:

public class ThreadState7 {

    private static final Object MONITOR = new Object();
    private static final DateTimeFormatter F = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) throws Exception {
        System.out.println(String.format("[%s]-begin...", F.format(LocalDateTime.now())));
        Thread thread1 = new Thread(() -> {
            synchronized (MONITOR) {
                System.out.println(String.format("[%s]-thread1 got monitor lock...", F.format(LocalDateTime.now())));
                try {
                    Thread.sleep(1000);
                    MONITOR.wait();
                } catch (InterruptedException e) {
                    //ignore
                }
                System.out.println(String.format("[%s]-thread1 exit waiting...", F.format(LocalDateTime.now())));
            }
        });
        Thread thread2 = new Thread(() -> {
            synchronized (MONITOR) {
                System.out.println(String.format("[%s]-thread2 got monitor lock...", F.format(LocalDateTime.now())));
                try {
                    MONITOR.notify();
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    //ignore
                }
                System.out.println(String.format("[%s]-thread2 releases monitor lock...", F.format(LocalDateTime.now())));
            }
        });
        thread1.start();
        thread2.start();
        // 這裡故意讓主執行緒sleep 1500毫秒從而讓thread2呼叫了Object#notify()並且尚未退出同步程式碼塊,確保thread1呼叫了Object#wait()
        Thread.sleep(1500);  
        System.out.println(thread1.getState());
        System.out.println(String.format("[%s]-end...", F.format(LocalDateTime.now())));
    }
}
// 某個時刻的輸出如下:
[2019-06-20 00:30:22]-begin...
[2019-06-20 00:30:22]-thread1 got monitor lock...
[2019-06-20 00:30:23]-thread2 got monitor lock...
BLOCKED
[2019-06-20 00:30:23]-end...
[2019-06-20 00:30:25]-thread2 releases monitor lock...
[2019-06-20 00:30:25]-thread1 exit waiting...

場景2中:

  • 執行緒2呼叫Object#notify()後睡眠2000毫秒再退出同步程式碼塊,釋放監視器鎖。
  • 執行緒1只睡眠了1000毫秒就呼叫了Object#wait(),此時它已經釋放了監視器鎖,所以執行緒2成功進入同步塊,執行緒1處於API註釋中所述的reenter a synchronized block/method的狀態。
  • 主執行緒睡眠1500毫秒剛好可以命中執行緒1處於reenter狀態並且列印其執行緒狀態,剛好就是BLOCKED狀態。

這三點看起來有點繞,多看幾次多思考一下應該就能理解。

TERMINATED狀態

API註釋

/**
 * Thread state for a terminated thread.
 * The thread has completed execution.
 */ 
TERMINATED;

終結的執行緒對應的執行緒狀態,此時執行緒已經執行完畢。

TERMINATED狀態表示執行緒已經終結。一個執行緒例項只能被啟動一次,準確來說,只會呼叫一次Thread#run()方法,Thread#run()方法執行結束之後,執行緒狀態就會更變為TERMINATED,意味著執行緒的生命週期已經結束。

舉個簡單的例子:

public class ThreadState8 {

    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(() -> {

        });
        thread.start();
        Thread.sleep(50);
        System.out.println(thread.getState());
    }
}
// 輸出結果
TERMINATED

上下文切換

多執行緒環境中,當一個執行緒的狀態由RUNNABLE轉換為非RUNNABLEBLOCKEDWAITING或者TIMED_WAITING)時,相應執行緒的上下文資訊(也就是常說的Context,包括CPU的暫存器和程式計數器在某一時間點的內容等等)需要被儲存,以便執行緒稍後恢復為RUNNABLE狀態時能夠在之前的執行進度的基礎上繼續執行。而一個執行緒的狀態由非RUNNABLE狀態進入RUNNABLE狀態時可能涉及恢復之前儲存的執行緒上下文資訊並且在此基礎上繼續執行。這裡的對執行緒的上下文資訊進行儲存和恢復的過程就稱為上下文切換(Context Switch)。

執行緒的上下文切換會帶來額外的效能開銷,這包括儲存和恢復執行緒上下文資訊的開銷、對執行緒進行排程的CPU時間開銷以及CPU快取內容失效的開銷(執行緒所執行的程式碼從CPU快取中訪問其所需要的變數值要比從主記憶體(RAM)中訪問響應的變數值要快得多,但是執行緒上下文切換會導致相關執行緒所訪問的CPU快取內容失效,一般是CPU的L1 CacheL2 Cache,使得相關執行緒稍後被重新排程到執行時其不得不再次訪問主記憶體中的變數以重新建立CPU快取內容)。

Linux系統中,可以通過vmstat命令來檢視全域性的上下文切換的次數,例如:

$ vmstat 1

對於Java程式的執行,在Linux系統中也可以通過perf命令進行監視,例如:

$ perf stat -e cpu-clock,task-clock,cs,cache-reference,cache-misses java YourJavaClass

參考資料中提到Windows系統下可以通過自帶的工具perfmon(其實也就是工作管理員)來監視執行緒的上下文切換,實際上筆者並沒有從工作管理員發現有任何辦法檢視上下文切換,通過搜尋之後發現了一個工具:Process Explorer。執行Process Explorer同時執行一個Java程式並且檢視其狀態:

j-t-l-s-4.png

因為打了斷點,可以看到執行中的程式的上下文切換一共7000多次,當前一秒的上下文切換增量為26(因為筆者設定了Process Explorer每秒重新整理一次資料)。

監控執行緒狀態

如果專案在生產環境中執行,不可能頻繁呼叫Thread#getState()方法去監測執行緒的狀態變化。JDK本身提供了一些監控執行緒狀態的工具,還有一些開源的輕量級工具如阿里的Arthas,這裡簡單介紹一下。

使用jvisualvm

jvisualvm是JDK自帶的堆、執行緒等待JVM指標監控工具,適合使用於開發和測試環境。它位於JAVA_HOME/bin目錄之下。

j-t-l-s-5.png

其中執行緒Dump的按鈕類似於下面要提到的jstack命令,用於匯出所有執行緒的棧資訊。

使用jstack

jstack是JDK自帶的命令列工具,功能是用於獲取指定PID的Java程式的執行緒棧資訊。例如本地執行的一個IDEA例項的PID是11376,那麼只需要輸入:

jstack 11376

然後控制檯輸出如下:

j-t-l-s-6.png

另外,如果想要定位具體Java程式的PID,可以使用jps命令。

使用JMC

JMC也就是Java Mission Control,它也是JDK自帶的工具,提供的功能要比jvisualvm強大,包括MBean的處理、執行緒棧已經狀態檢視、飛行記錄器等等。

j-t-l-s-7.png

小結

理解Java執行緒狀態的切換和一些監控手段,更有利於日常開發多執行緒程式,對於生產環境出現問題,通過監控執行緒的棧資訊能夠快速定位到問題的根本原因(通常來說,目前比較主流的MVC應用(準確來說應該是Servlet容器如Tomcat)都是通過一個執行緒處理一個單獨的請求,當請求出現阻塞的時候,匯出對應處理請求的執行緒基本可以定位到阻塞的精準位置,如果使用訊息佇列例如RabbitMQ,消費者執行緒出現阻塞也可以利用相似的思路解決)。

(本文完 e-a-20200804 c-3-d)

公眾號《Throwable》(id:throwable-doge),不定期推送架構設計、併發、原始碼探究相關的原創文章:

這是公眾號《Throwable》釋出的原創文章,收錄於專輯《併發程式設計》。

相關文章