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

前提

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

Java線程的實現

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

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

也就是一條Java線程就映射到一條輕量級進程(Light Weight Process)中,而一條輕量級線程又映射到一條內核線程(Kernel-Level Thread)。咱們平時所說的線程,每每就是指輕量級進程(或者說咱們平時新建的java.lang.Thread就是輕量級進程實例)。前面推算這個線程映射關係,能夠知道,咱們在應用程序中建立或者操做的java.lang.Thread實例最終會映射到系統的內核線程,若是咱們惡意或者實驗性無限建立java.lang.Thread實例,最終會影響系統的正常運行甚至致使系統崩潰(能夠在Windows開發環境中作實驗,確保內存足夠的狀況下使用死循環建立和運行java.lang.Thread實例)。shell

線程調度方式包括兩種,協同式線程調度和搶佔式線程調度。編程

線程調度方式 描述 劣勢 優點
協同式線程調度 線程的執行時間由線程自己控制,執行完畢後主動通知操做系統切換到另外一個線程上 某個線程若是不讓出CPU執行時間可能會致使整個系統崩潰 實現簡單,沒有線程同步的問題
搶佔式線程調度 每一個線程由操做系統來分配執行時間,線程的切換不禁線程自身決定 實現相對複雜,操做系統須要控制線程同步和切換 不會出現一個線程阻塞致使系統崩潰的問題

Java線程最終會映射爲系統內核原生線程,因此Java線程調度最終取決於系操做系統,而目前主流的操做系統內核線程調度基本都是使用搶佔式線程調度。也就是能夠死記硬背一下:Java線程是使用搶佔式線程調度方式進行線程調度的緩存

不少操做系統都提供線程優先級的概念,可是因爲平臺特性的問題,Java中的線程優先級和不一樣平臺中系統線程優先級並不匹配,因此Java線程優先級能夠僅僅理解爲「建議優先級」,通俗來講就是java.lang.Thread#setPriority(int newPriority)並不必定生效,有可能Java線程的優先級會被系統自行改變bash

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

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

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

監控線程狀態

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

使用jvisualvm

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

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

使用jstack

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

jstack 11376
複製代碼

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

使用JMC

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

小結

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

參考資料:

  • Jdk11相關源碼
  • 《Java多線程編程實戰指南》
  • 《深刻理解Java虛擬機-2nd》

連接

(本文完 c-7-d e-a-20190623 最近業務迭代有點忙)

相關文章
相關標籤/搜索