Java并發編程:深入理解Java線程狀態
在本文中,我們將深入探討 Java 線程的六種狀態以及它們之間如何相互轉換。線程狀態的轉換就如同生物從出生、成長到最終死亡的過程,也有一個完整的生命周期。
操作系統中的線程狀態
首先,讓我們看看操作系統中線程的生命周期是如何流轉的。
在操作系統中,線程共有 5 種狀態:
- 新建(NEW):線程已創建,但尚未開始執行。
- 就緒(READY):線程等待使用 CPU,在被調度程序調用后可進入運行狀態。
- 運行(RUNNING):線程正在使用 CPU。
- 等待(WAITING):線程因等待事件或其他資源(如 I/O)而被阻塞。
- 終止(TERMINATED):線程已完成執行。
Java 線程的 6 種狀態
Java 中線程狀態的定義與操作系統中的并不完全相同,查看 JDK 中的java.lang.Thread.State
可以找到 Java 線程狀態的定義:
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
它們之間的流程關系如下圖所示:
接下來,我們將對 Java 線程的六種狀態進行深入分析。
NEW(新建)
處于NEW
狀態的線程實際上還沒有啟動。也就是說,Thread 實例的start()
方法還沒有被調用。可流轉狀態:RUNNABLE
public class ThreadStateDemo {
public static void main(String[] args) {
Thread thread = new Thread(() -> {});
System.out.println(thread.getState());
}
}
輸出:
NEW
RUNNABLE(可運行)
Java 中的Runable
狀態對應操作系統線程狀態中的兩種狀態,分別是Running
和Ready
,也就是說,Java 中處于Runnable
狀態的線程有可能正在執行,也有可能沒有正在執行比如正在等待被分配 CPU 資源。
所以,如果一個正在運行的線程是Runnable
狀態,當它運行到任務的一半時,執行該線程的 CPU 被調度去做其他事情,導致該線程暫時不運行,它的狀態依然不變,還是Runnable
,因為它有可能隨時被調度回來繼續執行任務。可流轉狀態:BLOCKED
、WAITING
、TIMED_WAITING
、TERMINATED
在 Java 中,線程通過調用Thread
實例的start()
方法進入RUNNABLE
狀態。
關于start()
方法,有兩個問題需要思考一下:
- 能否對同一個線程重復調用
start()
方法? - 如果一個線程已經執行完畢并處于
TERMINATED
狀態,是否可以再次調用該線程的start()
方法?
為了分析這兩個問題,我們先來看看start()
方法的源碼:
public synchronized void start() {
if (threadStatus!= 0)
thrownew IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
我們可以看到,在start()
方法內部,有一個threadStatus
變量。如果它不等于 0,調用start()
方法將直接拋出異常。
接下來,調用了一個start0()
方法,但它是一個本地方法,無法知道方法內如何處理threadStatus
。但沒關系,我們可以在調用start()
方法后輸出當前狀態,并嘗試再次調用start()
方法:
public class ThreadStateDemo {
public static void main(String[] args) {
Thread thread = new Thread(() -> {});
System.out.println(thread.getState());
thread.start(); // 第一次調用
System.out.println(thread.getState());
thread.start(); // 第二次調用
}
}
輸出:
NEW
RUNNABLE
Exception in thread "main" java.lang.IllegalThreadStateException
at java.lang.Thread.start(Thread.java:708)
at thread.basic.ThreadStateDemo.main(ThreadStateDemo.java:11)
可以看到,第一次調用start()
方法是可以的,但第二次調用會報錯,java.lang.Thread.start(Thread.java:708)
指的是狀態檢查失敗:
查看獲取當前線程狀態的源碼:
public State getState() {
// 獲取當前線程狀態
return sun.misc.VM.toThreadState(threadStatus);
}
public static State toThreadState(int var0) {
if ((var0 & 4) != 0) {
return State.RUNNABLE;
} elseif ((var0 & 1024) != 0) {
return State.BLOCKED;
} elseif ((var0 & 16) != 0) {
return State.WAITING;
} elseif ((var0 & 32) != 0) {
return State.TIMED_WAITING;
} elseif ((var0 & 2) != 0) {
return State.TERMINATED;
} else {
return (var0 & 1) == 0 ? State.NEW : State.RUNNABLE;
}
}
我們可以看到,只有State.NEW
的狀態值被計算為 0。
因此,結合上面的源碼,我們可以得到兩個問題的答案都是不可行的。start()
方法只能在NEW
狀態下調用。
BLOCKED(阻塞)
處于BLOCKED
狀態的線程正在等待鎖的釋放。可流轉狀態:RUNNABLE
我們用一個生活中的例子來說明BLOCKED
狀態:
假設你去銀行辦理業務。當你來到某個窗口時,發現前面已經有人了。這時,你必須等待前面的人離開窗口,才能辦理業務。
假設你是線程 B,前面的人是線程 A。此時,A 占有了鎖(銀行辦理業務的窗口),B 正在等待鎖的釋放,線程 B 此時就處于 BLOCKED 狀態。
代碼示例如下:
public class BlockCase {
private synchronized void businessProcessing() {
try {
System.out.println("Thread[" + Thread.currentThread().getName() + "] performs business processing");
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
BlockCase blockCase = new BlockCase();
Thread A = new Thread(blockCase::businessProcessing, "A");
Thread B = new Thread(blockCase::businessProcessing, "B");
A.start();
B.start();
System.out.println("Thread[" + A.getName() + "] state:" + A.getState());
System.out.println("Thread[" + B.getName() + "] state:" + B.getState());
}
}
這里使用Thread.sleep()
來模擬業務處理所需的時間。
輸出:
Thread[A] performs business processing
Thread[A] state:RUNNABLE
Thread[B] state:BLOCKED
Thread[B] performs business processing
注意:如果多次執行輸出結果可能不相同,這是因為兩個線程誰先被調度是隨機的
WAITING(等待)
等待狀態。處于等待狀態的線程需要其他線程喚醒才能轉換為RUNNABLE
狀態。可流轉狀態:RUNNABLE
調用以下三種方法會使線程進入等待狀態:
Object.wait()
:使當前線程進入等待狀態,直到另一個線程喚醒它;Thread.join()
:等待指定的線程執行完畢。底層調用的是Object
實例的wait
方法;LockSupport.park()
:在獲得調用權限之前禁止當前線程進行線程調度。
我們主要解釋Object.wait()
和Thread.join()
的用法。
繼續前面的例子來解釋 WAITING 狀態:
你在銀行等了很久,終于輪到你來辦理業務了。但不幸的是,你到達柜臺后,柜臺的電腦突然壞了。你必須等待維修人員修好電腦后才能繼續辦理業務。
此時,假設你是線程 A,維修人員是線程 B。雖然你已經擁有了鎖(窗口),但你仍然需要釋放鎖。此時,線程 A 的狀態是 WAITING,然后線程 B 獲得鎖并進入 RUNNABLE 狀態。
如果線程 B 沒有主動喚醒線程 A(通過
notify()
或notifyAll()
),線程 A 只能一直等待。
Object.wait()
對于這個例子,我們使用wait()
、notify()
實現,如下所示:
public class WaitingCase {
private synchronized void businessProcessing() {
try {
System.out.println("Thread[" + Thread.currentThread().getName() + "] 處理業務,但電腦壞了。");
// 釋放窗口資源(鎖)
wait();
// 業務處理
System.out.println("Thread[" + Thread.currentThread().getName() + "] 繼續處理業務。");
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private synchronized void repairComputer() {
System.out.println("Thread[" + Thread.currentThread().getName() + "] 維修電腦。");
try {
// 模擬維修
Thread.sleep(1000);
System.out.println("Thread[" + Thread.currentThread().getName() + "] 電腦維修好了。");
notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
WaitingCase blockedCase = new WaitingCase();
Thread A = new Thread(blockedCase::businessProcessing, "A");
Thread B = new Thread(blockedCase::repairComputer, "B");
A.start();
Thread.sleep(500); // 用于確保線程 A 先搶到鎖。睡眠時間應該小于維修時間
B.start();
System.out.println("Thread[" + A.getName() + "] state:" + A.getState());
System.out.println("Thread[" + B.getName() + "] state:" + B.getState());
}
}
輸出:
Thread[A] 處理業務,但電腦壞了。
Thread[B] 維修電腦。
Thread[A] state:WAITING
Thread[B] state:TIMED_WAITING
Thread[B] 電腦維修好了。
Thread[A] 繼續處理業務。
關于wait()
方法,這里有一些需要注意的點:
- 線程在調用
wait()
方法之前必須持有對象的鎖。 - 當線程調用
wait()
方法時,它會釋放當前的鎖,直到另一個線程調用notify()
或notifyAll()
方法喚醒等待鎖的線程。 - 調用
notify()
方法只會喚醒一個等待鎖的線程。如果有多個線程在等待鎖,之前調用wait()
方法的線程可能不會被喚醒。 - 調用
notifyAll()
方法后,所有等待鎖的線程都會被喚醒,但時間片可能不會立即分配給剛剛放棄鎖的線程,這取決于系統的調度。
Thread.join()
join()
方法暫停調用線程的執行,直到被調用的對象完成執行。此時,當前線程處于WAITING
狀態。
join()
方法通常在主線程中使用,以等待其他線程完成后主線程再繼續執行。
現在來銀行辦理業務的人越來越多了,如果每次窗口空閑出來后所有人都會爭搶窗口的話,會造成資源的浪費。
銀行想到了一個辦法。每個來辦理業務的客戶都會得到一個序列號,窗口會依次叫號。只有被叫到的客戶才需要去窗口,否則他們可以留在休息區。
讓我們擴展前面BlockCase
中的例子來簡單實現這樣的功能:
public class JoinCase {
private synchronized void businessProcessing() {
try {
System.out.println("Thread[" + Thread.currentThread().getName() + "] 辦理業務。");
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
JoinCase blockedCase = new JoinCase();
Thread A = new Thread(blockedCase::businessProcessing, "A");
Thread B = new Thread(blockedCase::businessProcessing, "B");
Thread C = new Thread(blockedCase::businessProcessing, "C");
System.out.println("請讓線程 A 到窗口處理業務。");
A.start();
A.join();
System.out.println("請讓線程 B 到窗口處理業務。");
B.start();
B.join();
System.out.println("請讓線程 C 到窗口處理業務。");
C.start();
}
}
輸出:
請讓線程 A 到窗口處理業務。
Thread[A] 辦理業務。
請讓線程 B 到窗口處理業務。
Thread[B] 辦理業務。
請讓線程 C 到窗口處理業務。
Thread[C] 辦理業務。
你可以多次嘗試執行這個程序,每次都會得到相同的結果。
TIMED_WAITING(超時等待)
超時等待狀態。線程等待特定的時間,時間到了會自動喚醒。可流轉狀態:RUNNABLE
調用以下方法會使線程進入超時等待狀態:
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 狀態:
當你輪到你辦理業務員時,之前辦理業務的客戶說他忘記處理一個業務,現在需要處理,要求你給他 5 分鐘時間。你同意了然后就去休息區休息,當 5 分鐘過去后,你重新去辦理業務。
此時,你仍然是線程 A,插隊的朋友是線程 B。線程 B 讓線程 A 等待指定的時間,在這段等待期間,A 處于 TIMED_WAITING 狀態。
等待 5 分鐘后,A 自動喚醒,獲得了競爭鎖(窗口)的資格。
可以使用Object.wait(long timeout)
方法實現。Object.wait(long timeout)
方法與無參數的wait()
方法功能相同,都可以被其他線程調用notify()
或notifyAll()
方法喚醒。
public class TimedWaitingCase {
privatestaticfinal Object lock = new Object();
public static void main(String[] args) {
// 線程 A:模擬等待超時
Thread threadA = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("線程 A 開始等待,最多等待 5 秒...");
// 線程 A 進入 TIMED_WAITING 狀態,等待 5 秒
lock.wait(5000);
System.out.println("線程 A 等待結束,繼續執行。");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 線程 B:模擬在等待期間喚醒線程 A
Thread threadB = new Thread(() -> {
synchronized (lock) {
try {
// 線程 B 先睡眠 2 秒,模擬一些處理時間
Thread.sleep(2000);
System.out.println("線程 B 嘗試喚醒等待的線程 A...");
// 喚醒等待的線程 A
lock.notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 啟動線程 A
threadA.start();
// 啟動線程 B
threadB.start();
}
}
不同之處在于,帶參數的wait(long)
方法即使沒有其他線程喚醒它,也會在指定時間后自動喚醒,使其獲得競爭鎖的資格。
TERMINATED(終止)
再來看看最后一種狀態,Terminated
終止狀態,要想進入這個狀態有兩種可能。
run()
方法執行完畢,線程正常退出。- 出現一個沒有捕獲的異常,終止了
run()
方法,最終導致意外終止。
可流轉狀態:無
總結
Java 線程的六種狀態(NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED)描述了線程從創建到終止的完整生命周期。理解這些狀態及其轉換機制,有助于更好地掌握多線程編程,避免常見的并發問題。Java 線程狀態與操作系統線程狀態雖有相似之處,但 Java 對其進行了更細粒度的劃分,以適應復雜的并發場景。掌握這些狀態及其轉換,是編寫高效、穩定多線程程序的關鍵。