Java線程的6種狀態(tài)及轉(zhuǎn)化方式
前言
線程是 JVM 執(zhí)行任務(wù)的最小單元,理解線程的狀態(tài)轉(zhuǎn)換是理解后續(xù)多線程問題的基礎(chǔ)。
當(dāng)我們說一個(gè)線程的狀態(tài)時(shí),其實(shí)說的就是一個(gè)變量的值,在 Thread 類中的一個(gè)變量,叫 private volatile int threadStatus = 0;
這個(gè)值是個(gè)整數(shù),不方便理解,可以通過映射關(guān)系(VM.toThreadState),轉(zhuǎn)換成一個(gè)枚舉類。
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)建一個(gè)線程后,還沒有調(diào)用 start() 方法時(shí),此時(shí)這個(gè)線程的狀態(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)線程對(duì)象調(diào)用了 start() 方法之后,線程處于就緒狀態(tài),會(huì)存儲(chǔ)在一個(gè)就緒隊(duì)列中,就緒意味著該線程可以執(zhí)行,但具體啥時(shí)候執(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.
這里的意思是:
- 不允許對(duì)一個(gè)線程多次使用 start
- 線程執(zhí)行完成之后,不能試圖用 start 將其喚醒
那么在什么情況下會(huì)變成就緒狀態(tài)呢,如下情況:
- 線程調(diào)用 start(),新建狀態(tài)轉(zhuǎn)化為就緒狀態(tài)
- 線程 sleep(long) 時(shí)間到,等待狀態(tài)轉(zhuǎn)化為就緒狀態(tài)
- 阻塞式 IO 操作結(jié)果返回,線程變?yōu)榫途w狀態(tài)
- 其他線程調(diào)用 join() 方法,結(jié)束之后轉(zhuǎn)化為就緒狀態(tài)
- 線程對(duì)象拿到對(duì)象鎖之后,也會(huì)進(jìn)入就緒狀態(tài)
運(yùn)行狀態(tài)(RUNNING)
處于就緒狀態(tài)的線程獲得了 CPU 之后,真正開始執(zhí)行 run() 方法的線程執(zhí)行體時(shí),意味著該線程就已經(jīng)處于運(yùn)行狀態(tài)。需要注意的是,對(duì)于單處理器,一個(gè)時(shí)刻只能有一個(gè)線程處于運(yùn)行狀態(tài),對(duì)于搶占式策略的系統(tǒng)來說,系統(tǒng)會(huì)給每個(gè)線程一小段時(shí)間處理各自的任務(wù)。時(shí)間用完之后,系統(tǒng)負(fù)責(zé)奪回線程占用的資源。下一段時(shí)間里,系統(tǒng)會(huì)根據(jù)一定規(guī)則,再次進(jìn)行調(diào)度。
那么在什么時(shí)候運(yùn)行狀態(tài)變?yōu)榫途w狀態(tài)的呢?
- 線程失去處理器資源。線程不一定完整執(zhí)行的,執(zhí)行到一半,說不定就被別的線程搶走了
- 調(diào)用 yield() 靜態(tài)方法,暫時(shí)暫停當(dāng)前線程,讓系統(tǒng)的線程調(diào)度器重新調(diào)度一次,它自己完全有可能再次運(yùn)行
TERMINATED(終止?fàn)顟B(tài))
當(dāng)一個(gè)線程執(zhí)行完畢,線程的狀態(tài)就變?yōu)?TERMINATED。
TERMINATED 終止?fàn)顟B(tài),以下兩種情況會(huì)進(jìn)入這個(gè)狀態(tài):
- 當(dāng)線程的 run() 方法完成時(shí),或者主線程的 main() 方法完成時(shí),我們就認(rèn)為它終止了。這個(gè)線程對(duì)象也許是活的,但是它已經(jīng)不是一個(gè)單獨(dú)執(zhí)行的線程。線程一旦終止了,就不能復(fù)生
- 在一個(gè)終止的線程上調(diào)用 start() 方法,會(huì)拋出 java.lang.IllegalThreadStateException 異常
為什么會(huì)報(bào)錯(cuò)呢,因?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í)行,這個(gè)執(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 同步塊或者同步方法時(shí),如果獲取鎖失敗,則會(huì)進(jìn)入到 BLOCKED 狀態(tài)。當(dāng)獲取到鎖后,會(huì)從 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,這時(shí) B 線程處于 BLOCKED 狀態(tài),當(dāng) A 線程執(zhí)行完畢后,B 線程接著執(zhí)行對(duì)應(yīng)方法。
WAITING(等待狀態(tài))
這部分是比較復(fù)雜的,同時(shí)也是面試中問得最多的,處于這種狀態(tài)的線程不會(huì)被分配 CPU 執(zhí)行時(shí)間,它們要等待被顯式地喚醒,否則會(huì)處于無限期等待的狀態(tài)。
線程進(jìn)入 Waiting 狀態(tài)和回到 Runnable 有三種可能性:
wait/notify
沒有設(shè)置 Timeout 參數(shù)的 Object.wait() 方法,如果其他線程調(diào)用 notify() 或 notifyAll() 方法來喚醒它,它會(huì)直接進(jìn)入 Blocked 狀態(tài),因?yàn)閱拘?WAITING 線程的線程如果調(diào)用 notify() 或 notifyAll(),要求必須首先持有該 monitor 鎖,所以處于 WAITING 狀態(tài)的線程被喚醒時(shí)拿不到該鎖,就會(huì)進(jìn)入 Blocked 狀態(tài),直到執(zhí)行了 notify()/notifyAll() 方法喚醒它的線程執(zhí)行完畢并釋放 monitor 鎖,才可能輪到它去搶奪這把鎖,如果它能搶到,就會(huì)從 Blocked 狀態(tài)回到 Runnable 狀態(tài)。
這里的 notify 是只喚醒一個(gè)線程,而 notifyAll 是喚醒所有等待隊(duì)列中的線程。
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é)束之后,最后這句才會(huì)執(zhí)行,打印主線程名稱 System.out.println("線程1執(zhí)行完成,主線程"+Thread.currentThread().getName()+"繼續(xù)執(zhí)行"); } }
從上面的代碼中,當(dāng)執(zhí)行到 threadA.join() 的時(shí)候,(main)主線程會(huì)變成 WAITING 狀態(tài),直到線程 threadA 執(zhí)行完畢,主線程才會(huì)變回 RUNNABLE 狀態(tài),繼續(xù)往下執(zhí)行。
因此狀態(tài)圖如下:
而 join 阻塞主線程就是通過 wait 和 notifyAll 實(shí)現(xiàn)的,我們下面打開 join 的源碼看個(gè)究竟:
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 是同步方法,鎖對(duì)象是當(dāng)前對(duì)象即 threadA。所以這里是當(dāng)前線程(main 線程,因?yàn)槭?main 中調(diào)用的 join 方法)持有了 threadA 對(duì)象。isAlive() 是本地方法,非同步。然后判斷的是 threadA 線程是否存活。當(dāng) threadA 線程活著時(shí),調(diào)用wait(0) 方法,這是 Object 類的方法,Object 是超類,所以這里是使持有 threadA 對(duì)象的線程阻塞,即 main 線程阻塞。
從 RUNNABLE 到 WAITING,就和執(zhí)行了 wait() 方法完全一樣的,那么從 WAITING 回到 RUNNABLE 是怎么實(shí)現(xiàn)的呢?
就是被阻塞的主線程是如何被喚醒的呢?當(dāng)然是線程 threadA 結(jié)束后,由 jvm 自動(dòng)調(diào)用 t.notifyAll() 喚醒主線程。
那么怎么證明?
當(dāng)子線程執(zhí)行完 run 方法之后,底層在 jvm 源碼里,會(huì)執(zhí)行線程的 exit 方法,里面會(huì)調(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ī)在一個(gè)線程的方法執(zhí)行完畢后,執(zhí)行了個(gè) ensure_join 方法,這個(gè)就是專門為 join 而設(shè)計(jì)的。一步步跳進(jìn)方法中發(fā)現(xiàn)一段關(guān)鍵代碼,lock.notify_all,這便是一個(gè)線程結(jié)束后,會(huì)自動(dòng)調(diào)用自己的 notifyAll 方法的證明。
在這里我們可以總結(jié)一點(diǎn): join 就是 wait,線程結(jié)束就是 notifyAll。
LockSupport.park()/LockSupport.unpark(Thread)
理解了上面 wait 和 notify 的機(jī)制,下面就好理解了。
如果一個(gè)線程調(diào)用 LockSupport.park() 方法,則該線程狀態(tài)會(huì)從 RUNNABLE 變成 WAITING。
另一個(gè)線程調(diào)用 LockSupport.unpark(Thread) ,則剛剛的線程會(huì)從 WAITING 回到 RUNNABLE。
變化的狀態(tài)圖如下:
TIMED_WAITING(超時(shí)等待狀態(tài))
這部分就再簡(jiǎn)單不過了,超時(shí)等待與等待狀態(tài)一樣,唯一的區(qū)別就是將上面導(dǎo)致線程變成 WAITING 狀態(tài)的那些方法,都增加一個(gè)超時(shí)參數(shù)。
處于超時(shí)等待狀態(tài)中的線程不會(huì)被分配 CPU 執(zhí)行時(shí)間,必須等待其他相關(guān)線程執(zhí)行完特定的操作或者限時(shí)時(shí)間結(jié)束后,才有機(jī)會(huì)再次爭(zhēng)奪 CPU 使用權(quán),將超時(shí)等待狀態(tài)的線程轉(zhuǎn)換為運(yùn)行狀態(tài)。例如,調(diào)用了 wait(long timeout) 方法而處于等待狀態(tài)中的線程,需要通過其他線程調(diào)用 notify() 或者 notifyAll() 方法喚醒當(dāng)前等待中的線程,或者等待限時(shí)時(shí)間結(jié)束后也可以進(jìn)行狀態(tài)轉(zhuǎn)換。
變化的狀態(tài)圖如下:
以上就是整個(gè)線程狀態(tài)轉(zhuǎn)換圖,到此線程狀態(tài)轉(zhuǎn)換全部講解完。
總結(jié)
線程狀態(tài)轉(zhuǎn)換是寫好多線程代碼的基礎(chǔ),寫這篇文章目的是能夠更加清晰的理解線程狀態(tài)轉(zhuǎn)換,希望對(duì)小伙伴有所幫助。
最后來一張簡(jiǎn)化的線程轉(zhuǎn)換圖:
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
SharedingSphere?自定義脫敏規(guī)則介紹
這篇文章主要介紹了SharedingSphere?自定義脫敏規(guī)則,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12基于HTTP協(xié)議實(shí)現(xiàn)簡(jiǎn)單RPC框架的方法詳解
RPC全名(Remote?Procedure?Call),翻譯過來就是遠(yuǎn)程過程調(diào)用,本文將為大家介紹如何基于HTTP協(xié)議實(shí)現(xiàn)簡(jiǎn)單RPC框架,感興趣的小伙伴可以了解一下2023-06-06詳解Java的readBytes是怎么實(shí)現(xiàn)的
眾所周知,Java是一門跨平臺(tái)語言,針對(duì)不同的操作系統(tǒng)有不同的實(shí)現(xiàn),下面小編就來從一個(gè)非常簡(jiǎn)單的api調(diào)用帶大家來看看Java具體是怎么做的吧2023-07-07詳解Java實(shí)現(xiàn)緩存(LRU,FIFO)
本篇文章主要介紹了詳解Java實(shí)現(xiàn)緩存(LRU,FIFO) ,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-04-04