Java線程的6種狀態(tài)及轉(zhuǎn)化方式
前言
線程是 JVM 執(zhí)行任務(wù)的最小單元,理解線程的狀態(tài)轉(zhuǎn)換是理解后續(xù)多線程問題的基礎(chǔ)。
當(dāng)我們說一個線程的狀態(tài)時,其實(shí)說的就是一個變量的值,在 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(運(yùn)行狀態(tài))
當(dāng) Thread 調(diào)用 start 方法后,線程進(jìn)入 RUNNABLE 可運(yùn)行狀態(tài)

在 RUNNABLE 狀態(tài)當(dāng)中又包括了 RUNNING 和 READY 兩種狀態(tài)。
RUNNING(運(yùn)行中) 和 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)
- 線程對象拿到對象鎖之后,也會進(jìn)入就緒狀態(tài)
運(yùn)行狀態(tài)(RUNNING)
處于就緒狀態(tài)的線程獲得了 CPU 之后,真正開始執(zhí)行 run() 方法的線程執(zhí)行體時,意味著該線程就已經(jīng)處于運(yùn)行狀態(tài)。需要注意的是,對于單處理器,一個時刻只能有一個線程處于運(yùn)行狀態(tài),對于搶占式策略的系統(tǒng)來說,系統(tǒng)會給每個線程一小段時間處理各自的任務(wù)。時間用完之后,系統(tǒng)負(fù)責(zé)奪回線程占用的資源。下一段時間里,系統(tǒng)會根據(jù)一定規(guī)則,再次進(jìn)行調(diào)度。
那么在什么時候運(yùn)行狀態(tài)變?yōu)榫途w狀態(tài)的呢?
- 線程失去處理器資源。線程不一定完整執(zhí)行的,執(zhí)行到一半,說不定就被別的線程搶走了
- 調(diào)用 yield() 靜態(tài)方法,暫時暫停當(dāng)前線程,讓系統(tǒng)的線程調(diào)度器重新調(diào)度一次,它自己完全有可能再次運(yùn)行

TERMINATED(終止?fàn)顟B(tài))
當(dāng)一個線程執(zhí)行完畢,線程的狀態(tài)就變?yōu)?TERMINATED。
TERMINATED 終止?fàn)顟B(tài),以下兩種情況會進(jìn)入這個狀態(tài):
- 當(dāng)線程的 run() 方法完成時,或者主線程的 main() 方法完成時,我們就認(rèn)為它終止了。這個線程對象也許是活的,但是它已經(jīng)不是一個單獨(dú)執(zhí)行的線程。線程一旦終止了,就不能復(fù)生
- 在一個終止的線程上調(diào)用 start() 方法,會拋出 java.lang.IllegalThreadStateException 異常
為什么會報錯呢,因?yàn)?start 方法的已經(jīng)定義好了:
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
...
}
NEW、RUNNABLE、TERMINATED 案例
下面用代碼實(shí)現(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()+"線程運(yùn)行");
System.out.println("執(zhí)行完線程的狀態(tài)為:"+ thread .getState());
}創(chuàng)建線程后,線程的狀態(tài)為:NEW
調(diào)用start()方法后線程的狀態(tài)為:RUNNABLE
main線程運(yùn)行
執(zhí)行完線程的狀態(tài)為:TERMINATED
從以上代碼實(shí)現(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) 的線程進(jìn)入 synchronized 同步塊或者同步方法時,如果獲取鎖失敗,則會進(jìn)入到 BLOCKED 狀態(tài)。當(dāng)獲取到鎖后,會從 BLOCKED 狀態(tài)恢復(fù)到 RUNNABLE 狀態(tài)。
因此,我們可以得出如下轉(zhuǎn)換關(guān)系:

下面用代碼實(shí)現(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)。
線程進(jìn)入 Waiting 狀態(tài)和回到 Runnable 有三種可能性:
wait/notify
沒有設(shè)置 Timeout 參數(shù)的 Object.wait() 方法,如果其他線程調(diào)用 notify() 或 notifyAll() 方法來喚醒它,它會直接進(jìn)入 Blocked 狀態(tài),因?yàn)閱拘?WAITING 線程的線程如果調(diào)用 notify() 或 notifyAll(),要求必須首先持有該 monitor 鎖,所以處于 WAITING 狀態(tài)的線程被喚醒時拿不到該鎖,就會進(jìn)入 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 實(shí)現(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 線程,因?yàn)槭?main 中調(diào)用的 join 方法)持有了 threadA 對象。isAlive() 是本地方法,非同步。然后判斷的是 threadA 線程是否存活。當(dāng) threadA 線程活著時,調(diào)用wait(0) 方法,這是 Object 類的方法,Object 是超類,所以這里是使持有 threadA 對象的線程阻塞,即 main 線程阻塞。
從 RUNNABLE 到 WAITING,就和執(zhí)行了 wait() 方法完全一樣的,那么從 WAITING 回到 RUNNABLE 是怎么實(shí)現(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);
...
}虛擬機(jī)在一個線程的方法執(zhí)行完畢后,執(zhí)行了個 ensure_join 方法,這個就是專門為 join 而設(shè)計的。一步步跳進(jìn)方法中發(fā)現(xiàn)一段關(guān)鍵代碼,lock.notify_all,這便是一個線程結(jié)束后,會自動調(diào)用自己的 notifyAll 方法的證明。
在這里我們可以總結(jié)一點(diǎn): join 就是 wait,線程結(jié)束就是 notifyAll。

LockSupport.park()/LockSupport.unpark(Thread)
理解了上面 wait 和 notify 的機(jī)制,下面就好理解了。
如果一個線程調(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é)束后,才有機(jī)會再次爭奪 CPU 使用權(quán),將超時等待狀態(tài)的線程轉(zhuǎn)換為運(yùn)行狀態(tài)。例如,調(diào)用了 wait(long timeout) 方法而處于等待狀態(tài)中的線程,需要通過其他線程調(diào)用 notify() 或者 notifyAll() 方法喚醒當(dāng)前等待中的線程,或者等待限時時間結(jié)束后也可以進(jìn)行狀態(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)驗(yàn),希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
SharedingSphere?自定義脫敏規(guī)則介紹
這篇文章主要介紹了SharedingSphere?自定義脫敏規(guī)則,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12
基于HTTP協(xié)議實(shí)現(xiàn)簡單RPC框架的方法詳解
RPC全名(Remote?Procedure?Call),翻譯過來就是遠(yuǎn)程過程調(diào)用,本文將為大家介紹如何基于HTTP協(xié)議實(shí)現(xiàn)簡單RPC框架,感興趣的小伙伴可以了解一下2023-06-06
詳解Java的readBytes是怎么實(shí)現(xiàn)的
眾所周知,Java是一門跨平臺語言,針對不同的操作系統(tǒng)有不同的實(shí)現(xiàn),下面小編就來從一個非常簡單的api調(diào)用帶大家來看看Java具體是怎么做的吧2023-07-07
詳解Java實(shí)現(xiàn)緩存(LRU,FIFO)
本篇文章主要介紹了詳解Java實(shí)現(xiàn)緩存(LRU,FIFO) ,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-04-04

