今天是圣誕節,在這里祝各位同學圣誕快樂Merry Christmas!
🎄,但是學習這件事情還是得繼續哦,本章小宋講一下Java執行緒的狀態和主要的轉換方法,
目錄
- 作業系統中的執行緒狀態轉換
- Java執行緒的6個狀態
- NEW狀態
- 這里引申兩個start()的問題:
- RUNNABLE狀態
- JVM 為何不區分ready和running?
- BLOCKED狀態
- WAITING狀態
- TIMED_WAITING狀態
- TERMINATED狀態
- 執行緒狀態轉換
- BLOCKED與RUNNABLE狀態的轉換
- WAITING狀態與RUNNABLE狀態的轉換\
- Object.wait()方法
- Thread.join()
- TIMED_WAITING與RUNNABLE狀態轉換
- Thread.sleep(long)
- Object.wait(long)
- Thread.join(long)
- 執行緒中斷
作業系統中的執行緒狀態轉換
首先我們來看看作業系統中的執行緒狀態轉換,
現在的作業系統中,執行緒是被視為輕量級的行程,所以作業系統執行緒的狀態其實和作業系統行程的狀態是一致的,
如圖:

作業系統執行緒主要有以下三個狀態:
- 就緒狀態(ready): 執行緒正在等待使用CPU,經調度程式呼叫之后可進入running狀態,
- 執行狀態(running):執行緒正在使用CPU,
- 等待狀態(waiting): 執行緒經過等待事件的呼叫或者正在等待其他資源(如I/O),
Java執行緒的6個狀態

// Thread.State 原始碼
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
NEW狀態
處于NEW狀態的執行緒此時尚未啟動,這里的尚未啟動指的是還沒呼叫Thread實體的start()方法,
private void testStateNew() {
Thread thread = new Thread(() -> {});
System.out.println(thread.getState()); // 輸出 NEW
}
從上面可以看出,只是創建了執行緒而并沒有呼叫start()方法,此時執行緒處于NEW狀態,
這里引申兩個start()的問題:
- 反復呼叫同一個執行緒的start()方法是否可行?
- 假如一個執行緒執行完畢(此時處于Terminated狀態),再次呼叫這個執行緒的start()方法是否可行?
先來看一下start()的原始碼:

通過上面的start()原始碼我們可以發現有一個threadStatus的變數,start()中對它進行了判斷,如果threadStatus不等于0,呼叫start()是會直接拋出IllegalThreadStateException例外的,
接著往下看,有一個native的start0()方法,
這個方法里并沒有對threadStatus進行處理,到這里好像就拿這個threadStatus沒辦法了,我們debug看一下:
@Test
public void testStartMethod() {
Thread thread = new Thread(() -> {});
thread.start(); // 第一次呼叫
thread.start(); // 第二次呼叫
}
我是在start()方法內部的最開始打的斷點,敘述下我這里打斷點看到的結果:
- 第一次呼叫時threadStatus的值是0,
- 第二次呼叫時threadStatus的值不為0,
查看當前執行緒狀態的原始碼:


通過對上面原始碼的觀察可以得到剛剛提出的兩個問題的結果:
兩個問題的答案都是不可行,在呼叫一次start()之后,threadStatus的值會改變(threadStatus !=0),此時再次呼叫start()方法會拋出IllegalThreadStateException例外,
比如,threadStatus為2代表當前執行緒狀態為TERMINATED,
RUNNABLE狀態
表示當前執行緒正在運行中,處于RUNNABLE狀態的執行緒在Java虛擬機中運行,也有可能在就緒狀態等待CPU分配資源,
Java中執行緒的RUNNABLE狀態(看下Thread原始碼里對RUNNABLE狀態的定義):

Java執行緒的RUNNABLE狀態其實是包括了傳統作業系統執行緒的ready和running兩個狀態的,
這個時候可能會有人問了,為何 JVM 中沒有去區分這兩種狀態呢?
JVM 為何不區分ready和running?
現在的時分(time-sharing)多任務(multi-task)作業系統架構通常都是用所謂的時間分片(time quantum or time slice)方式進行搶占式(preemptive)輪轉調度(round-robin式),
更復雜一點可能會加入優先級(priority)的機制,
這個時間分片通常是很小的,一個執行緒一次最多只能在 cpu 上運行比如10-20ms 的時間(此時處于 running 狀態),也即大概只有0.01秒這一量級,時間片用后就要被切換下來放入調度佇列的末尾等待再次調度,(也即回到 ready 狀態)
如果期間進行了 I/O 的操作還會導致提前釋放時間分片,并進入等待佇列,
又或者是時間分片沒有用完就被搶占,這時也是回到 ready 狀態,
這一切換的程序稱為執行緒的背景關系切換(context switch),當然 cpu 不是簡單地把執行緒踢開就完了,還需要把被相應的執行狀態保存到記憶體(程式計數器)中以便后續的恢復執行,
顯然,10-20ms 對人而言是很快的,
如果不計切換開銷(每次在1ms 以內),相當于1秒內有50-100次切換,事實上時間片經常沒用完,執行緒就因為各種原因被中斷,實際發生的切換次數還會更多,
也這正是單核 CPU 上實作所謂的并發(concurrent)的基本原理,但其實是快速切換所帶來的假象,這有點類似一個手腳非常快的雜耍演員可以讓好多個球同時在空中運轉那般,
時間分片也是可配置的,如果不追求在多個執行緒間很快的回應,也可以把這個時間配置得大一點,以減少切換帶來的開銷,
如果是多核CPU,才有可能實作真正意義上的并發,這種情況通常也叫并行(parallel),
通常,Java的執行緒狀態是服務于監控的,如果執行緒切換得如此之快,那么區分 ready 與 running 就沒什么太大意義了,
當你看到監控上顯示是 running 時,對應的執行緒可能早就被切換下去了,甚至又再次地切換了上來,也許你只能看到 ready 與 running 兩個狀態在快速地閃爍,
當然,對于精確的性能評估而言,獲得準確的 running 時間是有必要的,
現今主流的 JVM 實作都把 Java 執行緒一一映射到作業系統底層的執行緒上,把調度委托給了作業系統,我們在虛擬機層面看到的狀態實質是對底層狀態的映射及包裝,JVM 本身沒有做什么實質的調度,把底層的 ready 及 running 狀態映射上來也沒多大意義,因此,統一成為runnable 狀態是不錯的選擇,
BLOCKED狀態
阻塞狀態,處于BLOCKED狀態的執行緒正等待鎖的釋放以進入同步區,
舉個生活中的例子:
假如今天你下班后準備去食堂吃飯,你來到食堂僅有的一個視窗,發現前面已經有個人在視窗前了,此時你必須得等前面的人從視窗離開才行,
假設你是執行緒t2,你前面的那個人是執行緒t1,此時t1占有了鎖(食堂唯一的視窗),t2正在等待鎖的釋放,所以此時t2就處于BLOCKED狀態,
WAITING狀態
等待狀態,處于等待狀態的執行緒變成RUNNABLE狀態需要其他執行緒喚醒,
呼叫如下3個方法會使執行緒進入等待狀態:
- Object.wait():使當前執行緒處于等待狀態直到另一個執行緒喚醒它;
- Thread.join():等待執行緒執行完畢,底層呼叫的是Object實體的wait方法;
- LockSupport.park():除非獲得呼叫許可,否則禁用當前執行緒進行執行緒調度,
我們延續上面的例子繼續解釋一下WAITING狀態:
你等了好幾分鐘現在終于輪到你了,突然你們有一個“不懂事”的經理突然來了,你看到他你就有一種不祥的預感,果然,他是來找你的,
他把你拉到一旁叫你待會兒再吃飯,說他下午要去作報告,趕緊來找你了解一下專案的情況,你心里雖然有一萬個不愿意但是你還是從食堂視窗走開了,
此時,假設你還是執行緒t2,你的經理是執行緒t1,雖然你此時都占有鎖(視窗)了,“不速之客”來了你還是得釋放掉鎖,此時你t2的狀態就是WAITING,然后經理t1獲得鎖,進入RUNNABLE狀態,
要是經理t1不主動喚醒你t2(notify、notifyAll..),可以說你t2只能一直等待了,
TIMED_WAITING狀態
超時等待狀態,執行緒等待一個具體的時間,時間到后會被自動喚醒,
呼叫如下方法會使執行緒進入超時等待狀態:
- Thread.sleep(long millis):使當前執行緒睡眠指定時間;
- Object.wait(long timeout):執行緒休眠指定時間,等待期間可以通過notify()/notifyAll()喚醒;
- Thread.join(long millis):等待當前執行緒最多執行millis毫秒,如果millis為0,則會一直執行;
- LockSupport.parkNanos(long nanos): 除非獲得呼叫許可,否則禁用當前執行緒進行執行緒調度指定時間;
- LockSupport.parkUntil(long deadline):同上,也是禁止執行緒進行調度指定時間;
我們繼續之前的例子來解釋一下TIMED_WAITING狀態:
到了第二天中午,又到了飯點,你還是到了視窗前,
突然間想起你的同事叫你等他一起,他說讓你等他十分鐘他改個bug,
好吧,你說那你就等等吧,你就離開了視窗,很快十分鐘過去了,你見他還沒來,你想都等了這么久了還不來,那你還是先去吃飯好了,
這時你還是執行緒t1,你改bug的同事是執行緒t2,t2讓t1等待了指定時間,此時t1等待期間就屬于TIMED_WATING狀態,
t1等待10分鐘后,就自動喚醒,擁有了去爭奪鎖的資格,
TERMINATED狀態
終止狀態,執行緒已執行完畢,
執行緒狀態轉換
上面講解了執行緒各個狀態的相關知識,下面我們繼續講執行緒狀態是如何轉換的,先看下面的圖:

BLOCKED與RUNNABLE狀態的轉換
上面說過處于BLOCKED狀態的執行緒是因為在等待鎖的釋放,假如這里有兩個執行緒a和b,a執行緒提前獲得了鎖并且暫未釋放鎖,此時b就處于BLOCKED狀態,看一個例子:
@Test
public void blockedTest() {
Thread a = new Thread(new Runnable() {
@Override
public void run() {
testMethod();
}
}, "a");
Thread b = new Thread(new Runnable() {
@Override
public void run() {
testMethod();
}
}, "b");
a.start();
b.start();
System.out.println(a.getName() + ":" + a.getState()); // 輸出?
System.out.println(b.getName() + ":" + b.getState()); // 輸出?
}
// 同步方法爭奪鎖
private synchronized void testMethod() {
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
初看之下,大家可能會覺得執行緒a會先呼叫同步方法,同步方法內又呼叫了Thread.sleep()方法,必然會輸出TIMED_WAITING,而執行緒b因為等待執行緒a釋放鎖所以必然會輸出BLOCKED,
其實不然,有兩點需要值得大家注意,一是在測驗方法blockedTest()內還有一個main執行緒,二是啟動執行緒后執行run方法還是需要消耗一定時間的,
測驗方法的main執行緒只保證了a,b兩個執行緒呼叫start()方法(轉化為RUNNABLE狀態),如果CPU執行效率高一點,還沒等兩個執行緒真正開始爭奪鎖,就已經列印此時兩個執行緒的狀態(RUNNABLE)了,
當然,如果CPU執行效率低一點,其中某個執行緒也是可能列印出BLOCKED狀態的(此時兩個執行緒已經開始爭奪鎖了),
那我想要列印出BLOCKED狀態該怎么處理呢?BLOCKED狀態的產生需要兩個執行緒爭奪鎖才行,那我們處理下測驗方法里的main執行緒就可以了,讓它“休息一會兒”,呼叫一下Thread.sleep()方法,
需要注意的是main執行緒休息的時間,要保證在執行緒爭奪鎖的時間內,不要等到前一個執行緒鎖都釋放了你再去爭奪鎖,此時還是得不到BLOCKED狀態的,
把上面的代碼稍稍修改下:
public void blockedTest() throws InterruptedException {
······
a.start();
Thread.sleep(1000L); // 需要注意這里main執行緒休眠了1000毫秒,而testMethod()里休眠了2000毫秒
b.start();
System.out.println(a.getName() + ":" + a.getState()); // 輸出?
System.out.println(b.getName() + ":" + b.getState()); // 輸出?
}
修改了以后兩個執行緒的狀態轉換如下
- a的狀態轉換程序:RUNNABLE(a.start()) -> TIMED_WATING(Thread.sleep())->RUNABLE(sleep()時間到)->BLOCKED(未搶到鎖) -> TERMINATED
- b的狀態轉換程序:RUNNABLE(b.start()) -> BLOCKED(未搶到鎖) ->TERMINATED
斜體表示可能出現的狀態, 大家可以在自己的電腦上多試幾次看看輸出,同樣,這里的輸出也可能有多鐘結果,
WAITING狀態與RUNNABLE狀態的轉換\
根據轉換圖我們知道有3個方法可以使執行緒從RUNNABLE狀態轉為WAITING狀態,這里主要介紹下Object.wait()和Thread.join(),
Object.wait()方法
呼叫wait()方法前執行緒必須持有物件的鎖,
執行緒呼叫wait()方法時,會釋放當前的鎖,直到有其他執行緒呼叫notify()/notifyAll()方法喚醒等待鎖的執行緒,
需要注意的是,其他執行緒呼叫notify()方法只會喚醒單個等待鎖的執行緒,如有有多個執行緒都在等待這個鎖的話不一定會喚醒到之前呼叫wait()方法的執行緒,
同樣,呼叫notifyAll()方法喚醒所有等待鎖的執行緒之后,也不一定會馬上把時間片分給剛才放棄鎖的那個執行緒,具體要看系統的調度,
Thread.join()
呼叫join()方法不會釋放鎖,會一直等待當前執行緒執行完畢(轉換為TERMINATED狀態),
修改下上面的代碼繼續距離 eg.
public void blockedTest() {
······
a.start();
a.join();
b.start();
System.out.println(a.getName() + ":" + a.getState()); // 輸出 TERMINATED
System.out.println(b.getName() + ":" + b.getState());
}
要是沒有呼叫join方法,main執行緒不管a執行緒是否執行完畢都會繼續往下走,
a執行緒啟動之后馬上呼叫了join方法,這里main執行緒就會等到a執行緒執行完畢,所以這里a執行緒列印的狀態固定是TERMINATED,
至于b執行緒的狀態,有可能列印RUNNABLE(尚未進入同步方法),也有可能列印TIMED_WAITING(進入了同步方法)
TIMED_WAITING與RUNNABLE狀態轉換
TIMED_WAITING與WAITING狀態類似,只是TIMED_WAITING狀態等待的時間是指定的,
Thread.sleep(long)
使當前執行緒睡眠指定時間,需要注意這里的“睡眠”只是暫時使執行緒停止執行,并不會釋放鎖,時間到后,執行緒會重新進入RUNNABLE狀態,
Object.wait(long)
wait(long)方法使執行緒進入TIMED_WAITING狀態,這里的wait(long)方法與無參方法wait()相同的地方是,都可以通過其他執行緒呼叫notify()或notifyAll()方法來喚醒,
不同的地方是,有參方法wait(long)就算其他執行緒不來喚醒它,經過指定時間long之后它會自動喚醒,擁有去爭奪鎖的資格,
Thread.join(long)
join(long)使當前執行緒執行指定時間,并且使執行緒進入TIMED_WAITING狀態,
繼續改上面的示例:
public void blockedTest() {
······
a.start();
a.join(1000L);
b.start();
System.out.println(a.getName() + ":" + a.getState()); // 輸出 TIEMD_WAITING
System.out.println(b.getName() + ":" + b.getState());
}
這里呼叫a.join(1000L),因為是指定了具體a執行緒執行的時間的,并且執行時間是小于a執行緒sleep的時間,所以a執行緒狀態輸出TIMED_WAITING,
b執行緒狀態仍然不固定(RUNNABLE或BLOCKED),
執行緒中斷
在某些情況下,我們在執行緒啟動后發現并不需要它繼續執行下去時,需要中斷執行緒,目前在Java里還沒有 安全直接的方法來停止執行緒,但是Java提供了執行緒中斷機制來處理需要中斷執行緒的情況,
執行緒中斷機制是一種協作機制,需要注意,通過中斷操作并不能直接終止一個執行緒,而是通知需要被中斷的執行緒自行處理,
簡單介紹下Thread類里提供的關于執行緒中斷的幾個方法:
- Thread.interrupt():中斷執行緒,這里的中斷執行緒并不會立即停止執行緒,而是設定執行緒的中斷狀態為true(默認是flase);
- Thread.interrupted():測驗當前執行緒是否被中斷,執行緒的中斷狀態受這個方法的影響,意思是呼叫一次使執行緒中斷狀態設定為true,連續呼叫兩次會使得這個執行緒的中斷狀態重新轉為false;
- Thread.isInterrupted():測驗當前執行緒是否被中斷,與上面方法不同的是呼叫這個方法并不會影響執行緒的中斷狀態,
在執行緒中斷機制里,當其他執行緒通知需要被中斷的執行緒后,執行緒中斷的狀態被設定為true,但是具體被要求中斷的執行緒要怎么處理,完全由被中斷執行緒自己而定,可以在合適的時候處理中斷請求,也可以完全不處理繼續執行下去,
講到這里本章對多執行緒系列之執行緒狀態和主要轉換方法的講解也就結束了,如果想了解更多知識可以在對應的專欄中看系列文章,謝謝大家的觀看,希望能給各位同學帶來幫助,如果覺得博主寫的還可以的,可以點贊收藏, 😉
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/240847.html
標籤:其他
