【轉載】Java線程生命週期與狀態切換

前提

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

Java線程的實現

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

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

也就是一條Java線程就映射到一條輕量級進程(Light Weight Process)中,而一條輕量級線程又映射到一條內核線程(Kernel-Level Thread)。我們平時所說的線程,往往就是指輕量級進程(或者說我們平時新建的java.lang.Thread就是輕量級進程實例)。前面推算這個線程映射關係,可以知道,我們在應用程序中創建或者操作的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;
}

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

線程狀態之間關係切換圖如下:

下面通過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.park() LockSupport.unpark(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

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

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

  • 線程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程序並且查看其狀態:

img

因爲打了斷點,可以看到運行中的程序的上下文切換一共7000多次,當前一秒的上下文切換增量爲26(因爲筆者設置了Process Explorer每秒刷新一次數據)。

監控線程狀態

如果項目在生產環境中運行,不可能頻繁調用Thread#getState()方法去監測線程的狀態變化。JDK本身提供了一些監控線程狀態的工具,還有一些開源的輕量級工具如阿里的Arthas,這裏簡單介紹一下。

使用jvisualvm

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

img

其中線程Dump的按鈕類似於下面要提到的jstack命令,用於導出所有線程的棧信息。

使用jstack

jstack是JDK自帶的命令行工具,功能是用於獲取指定PID的Java進程的線程棧信息。例如本地運行的一個IDEA實例的PID是11376,那麼只需要輸入:

複製jstack 11376

img

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

使用JMC

JMC也就是Java Mission Control,它也是JDK自帶的工具,提供的功能要比jvisualvm強大,包括MBean的處理、線程棧以及線程狀態查看、飛行記錄器等等。

img

小結

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

轉載地址

Java線程生命週期與狀態切換

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章