前言
Java語言定義了 6 種線程狀態,在任意一個時間點中,一個線程只能只且只有其中的一種狀態,並且可以通過特定的方法在不同狀態之間進行轉換。
今天,我們就詳細聊聊這幾種狀態,以及在什麼情況下會發生轉換。
一、線程狀態
要想知道Java線程都有哪些狀態,我們可以直接來看 Thread
,它有一個枚舉類 State
。
public class Thread {
public enum State {
/**
* 新建狀態
* 創建後尚未啓動的線程
*/
NEW,
/**
* 運行狀態
* 包括正在執行,也可能正在等待操作系統爲它分配執行時間
*/
RUNNABLE,
/**
* 阻塞狀態
* 一個線程因爲等待臨界區的鎖被阻塞產生的狀態
*/
BLOCKED,
/**
* 無限期等待狀態
* 線程不會被分配處理器執行時間,需要等待其他線程顯式喚醒
*/
WAITING,
/**
* 限期等待狀態
* 線程不會被分配處理器執行時間,但也無需等待被其他線程顯式喚醒
* 在一定時間之後,它們會由操作系統自動喚醒
*/
TIMED_WAITING,
/**
* 結束狀態
* 線程退出或已經執行完成
*/
TERMINATED;
}
}
二、狀態轉換
我們說,線程狀態並非是一成不變的,可以通過特定的方法在不同狀態之間進行轉換。那麼接下來,我們通過代碼,具體來看看這些個狀態是怎麼形成的。
1、新建
新建狀態最爲簡單,創建一個線程後,尚未啓動的時候就處於此種狀態。
public static void main(String[] args) {
Thread thread = new Thread("新建線程");
System.out.println("線程狀態:"+thread.getState());
}
-- 輸出:線程狀態:NEW
2、運行
可運行線程的狀態,當我們調用了start()
方法,線程正在Java虛擬機中執行,但它可能正在等待來自操作系統(如處理器)的其他資源。
所以,這裏實際上包含了兩種狀態:Running 和 Ready
,統稱爲 Runnable
。這是爲什麼呢?
這裏涉及到一個Java線程調度的問題:
線程調度,是指系統爲線程分配處理器使用權的過程。調度主要方式有兩種,協同式線程調度和搶佔式線程調度。
- 協同式線程調度
線程的執行時間由線程本身來控制,線程把自己的工作執行完畢之後,要主動通知系統切換到另外一個線程上去。
- 搶佔式線程調度
每個線程將由系統來自動分配執行時間,線程的切換不由線程本身來決定,是基於CPU時間分片的方式。
它們孰優孰劣,不在本文討論範圍之內。我們只需要知道,Java使用的線程調度方式就是搶佔式調度。
通常,這個時間分片是很小的,可能只有幾毫秒或幾十毫秒。所以,線程的實際狀態可能會在Running 和 Ready
狀態之間不斷變化。所以,再去區分它們意義不大。
那麼,我們再多想一下,如果Java線程調度方式是協同式調度,也許再去區分這兩個狀態就很有必要了。
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (;;){}
});
thread.start();
System.out.println("線程狀態:"+thread.getState());
}
-- 輸出:線程狀態:RUNNABLE
簡單來看,上面的代碼就使線程處於Runnable
狀態。但值得我們注意的是,如果一個線程在等待阻塞I/O的操作時,它的狀態也是Runnable
的。
我們來看兩個經典阻塞IO的例子:
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(() -> {
try {
ServerSocket serverSocket = new ServerSocket(9999);
while (true){
Socket socket = serverSocket.accept();
OutputStream outputStream = socket.getOutputStream();
outputStream.write("Hello".getBytes());
outputStream.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
},"accept");
t1.start();
Thread t2 = new Thread(() -> {
try {
Socket socket = new Socket("127.0.0.1",9999);
for (;;){
InputStream inputStream = socket.getInputStream();
byte[] bytes = new byte[5];
inputStream.read(bytes);
System.out.println(new String(bytes));
}
} catch (IOException e) {
e.printStackTrace();
}
},"read");
t2.start();
}
上面的代碼中,我們知道,serverSocket.accept()
和inputStream.read(bytes);
都是阻塞式方法。
它們一個在等待客戶端的連接;一個在等待數據的到來。但是,這兩個線程的狀態卻是 RUNNABLE
的。
"read" #13 prio=5 os_prio=0 tid=0x0000000023f6c800 nid=0x1cd0 runnable [0x0000000024b3e000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:171)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
"accept" #12 prio=5 os_prio=0 tid=0x0000000023f68000 nid=0x4cec runnable [0x0000000024a3e000]
java.lang.Thread.State: RUNNABLE
at java.net.DualStackPlainSocketImpl.accept0(Native Method)
at java.net.DualStackPlainSocketImpl.socketAccept(DualStackPlainSocketImpl.java:131)
at java.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:409)
at java.net.PlainSocketImpl.accept(PlainSocketImpl.java:199)
這又是爲什麼呢 ?
我們前面說過,處於 Runnable 狀態下的線程,正在 Java 虛擬機中執行,但它可能正在等待來自操作系統(如處理器)的其他資源
。
不管是CPU、網卡還是硬盤,這些都是操作系統的資源而已。當進行阻塞式的IO操作時,或許底層的操作系統線程確實處在阻塞狀態,但在這裏我們的 Java 虛擬機線程的狀態還是 Runnable
。
不要小看這個問題,很具有迷惑性。有些面試官如果問到,如果一個線程正在進行阻塞式 I/O 操作時,它處於什麼狀態?是Blocked還是Waiting?
那這時候,我們就要義正言辭的告訴他:親,都不是哦~
3、無限期等待
處於無限期等待狀態下的線程,不會被分配處理器執行時間,除非其他線程顯式的喚醒它。
最簡單的場景就是調用了 Object.wait()
方法。
public static void main(String[] args) throws Exception {
Object object = new Object();
new Thread(() -> {
synchronized (object){
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}}).start();
}
-- 輸出:線程狀態:WAITING
此時這個線程就處於無限期等待狀態,除非有別的線程顯式的調用object.notifyAll();
來喚醒它。
然後,就是Thread.join()
方法,當主線程調用了此方法,就必須等待子線程結束之後才能繼續進行。
public static void main(String[] args) throws Exception {
Thread mainThread = new Thread(() -> {
Thread subThread = new Thread(() -> {
for (;;){}
});
subThread.start();
try {
subThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
mainThread.start();
System.out.println("線程狀態:"+thread.getState());
}
//輸出:線程狀態:WAITING
如上代碼,在主線程 mainThread
中調用了子線程的join()
方法,那麼主線程就要等待子線程結束運行。所以此時主線程mainThread
的狀態就是無限期等待。
多說一句,其實join()
方法內部,調用的也是Object.wait()
。
最後,我們說說LockSupport.park()
方法,它同樣會使線程進入無限期等待狀態。也許有的朋友對它很陌生,沒有用過,我們來看一個阻塞隊列的例子。
public static void main(String[] args) throws Exception {
ArrayBlockingQueue<Long> queue = new ArrayBlockingQueue(1);
Thread thread = new Thread(() -> {
while (true){
try {
queue.put(System.currentTimeMillis());
} catch (Exception e) {
e.printStackTrace();
}
}
});
thread.start();
}
如上代碼,往往我們會通過阻塞隊列的方式來做生產者-消費者模型的代碼。
這裏,ArrayBlockingQueue
長度爲1,當我們第二次往裏面添加數據的時候,發現隊列已滿,線程就會等待這裏,它的源碼裏面正是調用了LockSupport.park()
。
同樣的,這裏也比較具有迷惑性,我來問你:阻塞隊列中,如果隊列爲空或者隊列已滿,這時候執行take或者put操作的時候,線程的狀態是 Blocked 嗎?
那這時候,我們需要謹記這裏的線程狀態還是 WAITING
。它們之間的區別和聯繫,我們後文再看。
4、限期等待
同樣的,處於限期等待狀態下的線程,也不會被分配處理器執行時間,但是它在一定時間之後可以自動的被操作系統喚醒。
這個跟無限期等待的區別,僅僅就是有沒有帶有超時時間參數。
比如:
object.wait(3000);
thread.join(3000);
LockSupport.parkNanos(5000000L);
Thread.sleep(1000);
像這種操作,都會使線程處於限期等待的狀態 TIMED_WAITING
。因爲Thread.sleep()
必須帶有時間參數,所以它不在無限期等待行列中。
5、阻塞
一個線程因爲等待臨界區的鎖被阻塞產生的狀態,也就是說,阻塞狀態的產生是因爲它正在等待着獲取一個排它鎖。
這裏,我們來看一個 synchronized
的例子。
public static void main(String[] args) throws Exception {
Object object = new Object();
Thread t1 = new Thread(() -> {
synchronized (object){
for (;;){}
}
});
t1.start();
Thread t2 = new Thread(() -> {
synchronized (object){
System.out.println("獲取到object鎖,線程執行。");
}
});
t2.start();
System.out.println("線程狀態:"+t2.getState());
}
//輸出:線程狀態:BLOCKED
我們看上面的代碼,object對象鎖一直被線程 t1 持有,所以線程 t2 的狀態一直會是阻塞狀態。
我們接着再來看一個鎖的例子:
public static void main(String[] args){
Lock lock = new ReentrantLock();
lock.lock();
Thread t1 = new Thread(() -> {
lock.lock();
System.out.println("已獲取lock鎖,線程執行");
lock.unlock();
});
t1.start();
System.out.println("線程狀態:"+t1.getState());
}
如上代碼,我們有一個ReentrantLock
,main線程已經持有了這個鎖,t1 線程會一直等待在lock.lock();
。
那麼,此時 t1 線程的狀態是什麼呢 ?
其實答案是WAITING
,即無限期等待狀態。這又是爲什麼呢 ?
原因在於,Lock
接口是Java API實現的鎖,它的底層實現其實是抽象同步隊列,簡稱AQS
。
在通過lock.lock()
獲取鎖的時候,如果鎖正在被其他線程持有,那麼線程會被放入AQS隊列後,阻塞掛起。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
如果tryAcquire返回false,會把當前線程放入AQS阻塞隊列
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquireQueued
方法會將當前線程放入 AQS 阻塞隊列,然後調用LockSupport.park(this);
掛起線程。
所以,這也就解釋了爲什麼lock.lock()
獲取鎖的時候,當前的線程狀態會是 WAITING
。
常常有人會問,synchronized和Lock
的區別,除了一般性的答案,此時你也可以說一下線程狀態的差異,我猜可能很少有人會意識到這一點。
6、結束
一個線程,當它退出或已經執行完成的時候,就是結束狀態。
public static void main(String[] args) throws Exception {
Thread thread = new Thread(() -> System.out.println("線程已執行"));
thread.start();
Thread.sleep(1000);
System.out.println("線程狀態:"+thread.getState());
}
//輸出: 線程已執行
線程狀態:TERMINATED
三、總結
本文介紹了 Java 線程的不同狀態,以及在何種情況下發生轉換。
原創不易,客官們點個贊再走嘛,這將是筆者持續寫作的動力~