java不同进程的相互唤醒_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實例會對應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; }這些狀態的描述總結成圖如下:
「線程狀態之間關系切換」圖如下:
下面通過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());} }// 輸出結果 NEWRUNNABLE狀態
「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狀態可以認為包含兩個子狀態:READY和RUNNING。
- 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());} } // 輸出結果 RUNNABLEWAITING狀態
「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 RUNNABLETIMED 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 TERMINATEDBLOCKED狀態
「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?
?
更加詳細的描述可以參考筆者之前寫過的一篇文章:深入理解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轉換為非RUNNABLE(BLOCKED、WAITING或者TIMED_WAITING)時,相應線程的上下文信息(也就是常說的Context,包括CPU的寄存器和程序計數器在某一時間點的內容等等)需要被保存,以便線程稍后恢復為RUNNABLE狀態時能夠在之前的執行進度的基礎上繼續執行。而一個線程的狀態由非RUNNABLE狀態進入RUNNABLE狀態時可能涉及恢復之前保存的線程上下文信息并且在此基礎上繼續執行。這里的對「線程的上下文信息進行保存和恢復的過程」就稱為上下文切換(Context Switch)。
線程的上下文切換會帶來額外的性能開銷,這包括保存和恢復線程上下文信息的開銷、對線程進行調度的CPU時間開銷以及CPU緩存內容失效的開銷(線程所執行的代碼從CPU緩存中訪問其所需要的變量值要比從主內存(RAM)中訪問響應的變量值要快得多,但是「線程上下文切換會導致相關線程所訪問的CPU緩存內容失效,一般是CPU的L1 Cache和L2 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,這里簡單介紹一下。
使用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應用(準確來說應該是Servlet容器如Tomcat)都是通過一個線程處理一個單獨的請求,當請求出現阻塞的時候,導出對應處理請求的線程基本可以定位到阻塞的精準位置,如果使用消息隊列例如RabbitMQ,消費者線程出現阻塞也可以利用相似的思路解決)。
原文出自公眾號:Throwable原文鏈接:https://mp.weixin.qq.com/s/BAOt4zXkRGJbUliJXJQMUg
總結
以上是生活随笔為你收集整理的java不同进程的相互唤醒_Java线程生命周期与状态切换的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2023 款比亚迪秦 PLUS DM-i
- 下一篇: java excel 操作 poi_Ja