Java線程的6種狀態(tài)及轉(zhuǎn)化方式
前言
線程是 JVM 執(zhí)行任務(wù)的最小單元,理解線程的狀態(tài)轉(zhuǎn)換是理解后續(xù)多線程問題的基礎(chǔ)。
當(dāng)我們說一個線程的狀態(tài)時,其實說的就是一個變量的值,在 Thread 類中的一個變量,叫 private volatile int threadStatus = 0;
這個值是個整數(shù),不方便理解,可以通過映射關(guān)系(VM.toThreadState),轉(zhuǎn)換成一個枚舉類。
public enum State { NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED; }
java.lang.Thread.State 枚舉類中定義了 6 種線程的狀態(tài),可以調(diào)用線程 Thread 中的 getState() 方法獲取當(dāng)前線程的狀態(tài)。
Java線程狀態(tài)轉(zhuǎn)換圖
NEW (新建狀態(tài))
當(dāng)創(chuàng)建一個線程后,還沒有調(diào)用 start() 方法時,此時這個線程的狀態(tài),是 NEW(初始態(tài))
Thread t = new Thread();
RUNNABLE(運行狀態(tài))
當(dāng) Thread 調(diào)用 start 方法后,線程進入 RUNNABLE 可運行狀態(tài)
在 RUNNABLE 狀態(tài)當(dāng)中又包括了 RUNNING 和 READY 兩種狀態(tài)。
RUNNING(運行中) 和 READY(就緒)
就緒狀態(tài)(READY)
當(dāng)線程對象調(diào)用了 start() 方法之后,線程處于就緒狀態(tài),會存儲在一個就緒隊列中,就緒意味著該線程可以執(zhí)行,但具體啥時候執(zhí)行將取決于 JVM 里線程調(diào)度器的調(diào)度。
It is never legal to start a thread more than once. In particular, a thread may not be restarted once it has completed execution.
這里的意思是:
- 不允許對一個線程多次使用 start
- 線程執(zhí)行完成之后,不能試圖用 start 將其喚醒
那么在什么情況下會變成就緒狀態(tài)呢,如下情況:
- 線程調(diào)用 start(),新建狀態(tài)轉(zhuǎn)化為就緒狀態(tài)
- 線程 sleep(long) 時間到,等待狀態(tài)轉(zhuǎn)化為就緒狀態(tài)
- 阻塞式 IO 操作結(jié)果返回,線程變?yōu)榫途w狀態(tài)
- 其他線程調(diào)用 join() 方法,結(jié)束之后轉(zhuǎn)化為就緒狀態(tài)
- 線程對象拿到對象鎖之后,也會進入就緒狀態(tài)
運行狀態(tài)(RUNNING)
處于就緒狀態(tài)的線程獲得了 CPU 之后,真正開始執(zhí)行 run() 方法的線程執(zhí)行體時,意味著該線程就已經(jīng)處于運行狀態(tài)。需要注意的是,對于單處理器,一個時刻只能有一個線程處于運行狀態(tài),對于搶占式策略的系統(tǒng)來說,系統(tǒng)會給每個線程一小段時間處理各自的任務(wù)。時間用完之后,系統(tǒng)負(fù)責(zé)奪回線程占用的資源。下一段時間里,系統(tǒng)會根據(jù)一定規(guī)則,再次進行調(diào)度。
那么在什么時候運行狀態(tài)變?yōu)榫途w狀態(tài)的呢?
- 線程失去處理器資源。線程不一定完整執(zhí)行的,執(zhí)行到一半,說不定就被別的線程搶走了
- 調(diào)用 yield() 靜態(tài)方法,暫時暫停當(dāng)前線程,讓系統(tǒng)的線程調(diào)度器重新調(diào)度一次,它自己完全有可能再次運行
TERMINATED(終止?fàn)顟B(tài))
當(dāng)一個線程執(zhí)行完畢,線程的狀態(tài)就變?yōu)?TERMINATED。
TERMINATED 終止?fàn)顟B(tài),以下兩種情況會進入這個狀態(tài):
- 當(dāng)線程的 run() 方法完成時,或者主線程的 main() 方法完成時,我們就認(rèn)為它終止了。這個線程對象也許是活的,但是它已經(jīng)不是一個單獨執(zhí)行的線程。線程一旦終止了,就不能復(fù)生
- 在一個終止的線程上調(diào)用 start() 方法,會拋出 java.lang.IllegalThreadStateException 異常
為什么會報錯呢,因為 start 方法的已經(jīng)定義好了:
public synchronized void start() { if (threadStatus != 0) throw new IllegalThreadStateException(); ... }
NEW、RUNNABLE、TERMINATED 案例
下面用代碼實現(xiàn)看看:
public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(); System.out.println("創(chuàng)建線程后,線程的狀態(tài)為:"+ thread .getState()); myThread.start(); System.out.println("調(diào)用start()方法后線程的狀態(tài)為:"+thread .getState()); //休眠30毫秒,等待 Thread 線程執(zhí)行完 Thread.sleep(30); System.out.println(Thread.currentThread().getName()+"線程運行"); System.out.println("執(zhí)行完線程的狀態(tài)為:"+ thread .getState()); }
創(chuàng)建線程后,線程的狀態(tài)為:NEW
調(diào)用start()方法后線程的狀態(tài)為:RUNNABLE
main線程運行
執(zhí)行完線程的狀態(tài)為:TERMINATED
從以上代碼實現(xiàn)可知:
- 剛創(chuàng)建完線程后,狀態(tài)為 NEW
- 調(diào)用了 start() 方法后線程的狀態(tài)變?yōu)?RUNNABLE
- 然后,我們看到了 run() 方法的執(zhí)行,這個執(zhí)行,是在主線程 main 中調(diào)用 start() 方法后線程的狀態(tài)為 RUNNABLE 輸出后執(zhí)行的
- 隨后,我們讓 main 線程休眠了30毫秒,等待 Thread 線程退出
- 最后再打印 Thread 線程的狀態(tài),為 TERMINATED
BLOCKED (阻塞狀態(tài))
在 RUNNABLE狀態(tài) 的線程進入 synchronized 同步塊或者同步方法時,如果獲取鎖失敗,則會進入到 BLOCKED 狀態(tài)。當(dāng)獲取到鎖后,會從 BLOCKED 狀態(tài)恢復(fù)到 RUNNABLE 狀態(tài)。
因此,我們可以得出如下轉(zhuǎn)換關(guān)系:
下面用代碼實現(xiàn)看看:
public static synchronized void method01() { System.out.println(Thread.currentThread().getName()+"開始執(zhí)行主線程的方法"); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"主線程的方法執(zhí)行完畢"); } public static void main(String[] args) throws InterruptedException { Thread threadA = new Thread(() -> method01(), "A-Thread"); Thread threadB = new Thread(() -> method01(), "B-Thread"); threadA.start(); threadB.start(); System.out.println("線程 A 的狀態(tài)為:"+ threadA.getState()); System.out.println("線程 B 的狀態(tài)為:"+ threadB.getState()); }
A-Thread開始執(zhí)行主線程的方法
線程A的狀態(tài)為:RUNNABLE
線程B的狀態(tài)為:BLOCKED
A-Thread主線程的方法執(zhí)行完畢
B-Thread開始執(zhí)行主線程的方法
B-Thread主線程的方法執(zhí)行完畢
從上面代碼執(zhí)行可知,A 線程優(yōu)先獲得到了鎖,狀態(tài)為 RUNNABLE,這時 B 線程處于 BLOCKED 狀態(tài),當(dāng) A 線程執(zhí)行完畢后,B 線程接著執(zhí)行對應(yīng)方法。
WAITING(等待狀態(tài))
這部分是比較復(fù)雜的,同時也是面試中問得最多的,處于這種狀態(tài)的線程不會被分配 CPU 執(zhí)行時間,它們要等待被顯式地喚醒,否則會處于無限期等待的狀態(tài)。
線程進入 Waiting 狀態(tài)和回到 Runnable 有三種可能性:
wait/notify
沒有設(shè)置 Timeout 參數(shù)的 Object.wait() 方法,如果其他線程調(diào)用 notify() 或 notifyAll() 方法來喚醒它,它會直接進入 Blocked 狀態(tài),因為喚醒 WAITING 線程的線程如果調(diào)用 notify() 或 notifyAll(),要求必須首先持有該 monitor 鎖,所以處于 WAITING 狀態(tài)的線程被喚醒時拿不到該鎖,就會進入 Blocked 狀態(tài),直到執(zhí)行了 notify()/notifyAll() 方法喚醒它的線程執(zhí)行完畢并釋放 monitor 鎖,才可能輪到它去搶奪這把鎖,如果它能搶到,就會從 Blocked 狀態(tài)回到 Runnable 狀態(tài)。
這里的 notify 是只喚醒一個線程,而 notifyAll 是喚醒所有等待隊列中的線程。
join
public class Test { class ThreadA extends Thread{ @Override public void run() { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("線程1執(zhí)行完成"); } } public static void main(String[] args) throws InterruptedException { Test threadClass = new Test(); ThreadA threadA = threadClass.new ThreadA(); threadA.start(); threadA.join(); //threadA 線程結(jié)束之后,最后這句才會執(zhí)行,打印主線程名稱 System.out.println("線程1執(zhí)行完成,主線程"+Thread.currentThread().getName()+"繼續(xù)執(zhí)行"); } }
從上面的代碼中,當(dāng)執(zhí)行到 threadA.join() 的時候,(main)主線程會變成 WAITING 狀態(tài),直到線程 threadA 執(zhí)行完畢,主線程才會變回 RUNNABLE 狀態(tài),繼續(xù)往下執(zhí)行。
因此狀態(tài)圖如下:
而 join 阻塞主線程就是通過 wait 和 notifyAll 實現(xiàn)的,我們下面打開 join 的源碼看個究竟:
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) { //這里的this.isAlive,判斷條件是子線程是否活著,如果活著,當(dāng)前執(zhí)行線程主線程就 wait 阻塞。 while (isAlive()) { wait(0); } } else { //這里的this.isAlive,判斷條件是子線程是否活著,如果活著,當(dāng)前執(zhí)行線程主線程就 wait 阻塞。 while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay); now = System.currentTimeMillis() - base; } } }
從源碼中,可以看到 join 是同步方法,鎖對象是當(dāng)前對象即 threadA。所以這里是當(dāng)前線程(main 線程,因為是 main 中調(diào)用的 join 方法)持有了 threadA 對象。isAlive() 是本地方法,非同步。然后判斷的是 threadA 線程是否存活。當(dāng) threadA 線程活著時,調(diào)用wait(0) 方法,這是 Object 類的方法,Object 是超類,所以這里是使持有 threadA 對象的線程阻塞,即 main 線程阻塞。
從 RUNNABLE 到 WAITING,就和執(zhí)行了 wait() 方法完全一樣的,那么從 WAITING 回到 RUNNABLE 是怎么實現(xiàn)的呢?
就是被阻塞的主線程是如何被喚醒的呢?當(dāng)然是線程 threadA 結(jié)束后,由 jvm 自動調(diào)用 t.notifyAll() 喚醒主線程。
那么怎么證明?
當(dāng)子線程執(zhí)行完 run 方法之后,底層在 jvm 源碼里,會執(zhí)行線程的 exit 方法,里面會調(diào)用 notifyAll 方法。
hotspot/src/share/vm/runtime/thread.cpp void JavaThread::exit(...) { ... ensure_join(this); ... }
static void ensure_join(JavaThread* thread) { ... lock.notify_all(thread); ... }
虛擬機在一個線程的方法執(zhí)行完畢后,執(zhí)行了個 ensure_join 方法,這個就是專門為 join 而設(shè)計的。一步步跳進方法中發(fā)現(xiàn)一段關(guān)鍵代碼,lock.notify_all,這便是一個線程結(jié)束后,會自動調(diào)用自己的 notifyAll 方法的證明。
在這里我們可以總結(jié)一點: join 就是 wait,線程結(jié)束就是 notifyAll。
LockSupport.park()/LockSupport.unpark(Thread)
理解了上面 wait 和 notify 的機制,下面就好理解了。
如果一個線程調(diào)用 LockSupport.park() 方法,則該線程狀態(tài)會從 RUNNABLE 變成 WAITING。
另一個線程調(diào)用 LockSupport.unpark(Thread) ,則剛剛的線程會從 WAITING 回到 RUNNABLE。
變化的狀態(tài)圖如下:
TIMED_WAITING(超時等待狀態(tài))
這部分就再簡單不過了,超時等待與等待狀態(tài)一樣,唯一的區(qū)別就是將上面導(dǎo)致線程變成 WAITING 狀態(tài)的那些方法,都增加一個超時參數(shù)。
處于超時等待狀態(tài)中的線程不會被分配 CPU 執(zhí)行時間,必須等待其他相關(guān)線程執(zhí)行完特定的操作或者限時時間結(jié)束后,才有機會再次爭奪 CPU 使用權(quán),將超時等待狀態(tài)的線程轉(zhuǎn)換為運行狀態(tài)。例如,調(diào)用了 wait(long timeout) 方法而處于等待狀態(tài)中的線程,需要通過其他線程調(diào)用 notify() 或者 notifyAll() 方法喚醒當(dāng)前等待中的線程,或者等待限時時間結(jié)束后也可以進行狀態(tài)轉(zhuǎn)換。
變化的狀態(tài)圖如下:
以上就是整個線程狀態(tài)轉(zhuǎn)換圖,到此線程狀態(tài)轉(zhuǎn)換全部講解完。
總結(jié)
線程狀態(tài)轉(zhuǎn)換是寫好多線程代碼的基礎(chǔ),寫這篇文章目的是能夠更加清晰的理解線程狀態(tài)轉(zhuǎn)換,希望對小伙伴有所幫助。
最后來一張簡化的線程轉(zhuǎn)換圖:
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
SharedingSphere?自定義脫敏規(guī)則介紹
這篇文章主要介紹了SharedingSphere?自定義脫敏規(guī)則,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12基于HTTP協(xié)議實現(xiàn)簡單RPC框架的方法詳解
RPC全名(Remote?Procedure?Call),翻譯過來就是遠程過程調(diào)用,本文將為大家介紹如何基于HTTP協(xié)議實現(xiàn)簡單RPC框架,感興趣的小伙伴可以了解一下2023-06-06