前言
Java語言定義了 6 種線程狀態(tài)植影,在任意一個時間點中涎永,一個線程只能只且只有其中的一種狀態(tài)羡微,并且可以通過特定的方法在不同狀態(tài)之間進行轉(zhuǎn)換。
今天博投,我們就詳細聊聊這幾種狀態(tài)盯蝴,以及在什么情況下會發(fā)生轉(zhuǎn)換捧挺。
一、線程狀態(tài)
要想知道Java線程都有哪些狀態(tài)翅睛,我們可以直接來看 Thread
,它有一個枚舉類 State
。
public class Thread {
public enum State {
/**
* 新建狀態(tài)
* 創(chuàng)建后尚未啟動的線程
*/
NEW,
/**
* 運行狀態(tài)
* 包括正在執(zhí)行爬骤,也可能正在等待操作系統(tǒng)為它分配執(zhí)行時間
*/
RUNNABLE,
/**
* 阻塞狀態(tài)
* 一個線程因為等待臨界區(qū)的鎖被阻塞產(chǎn)生的狀態(tài)
*/
BLOCKED,
/**
* 無限期等待狀態(tài)
* 線程不會被分配處理器執(zhí)行時間莫换,需要等待其他線程顯式喚醒
*/
WAITING,
/**
* 限期等待狀態(tài)
* 線程不會被分配處理器執(zhí)行時間拉岁,但也無需等待被其他線程顯式喚醒
* 在一定時間之后,它們會由操作系統(tǒng)自動喚醒
*/
TIMED_WAITING,
/**
* 結(jié)束狀態(tài)
* 線程退出或已經(jīng)執(zhí)行完成
*/
TERMINATED;
}
}
二惫企、狀態(tài)轉(zhuǎn)換
我們說陵叽,線程狀態(tài)并非是一成不變的巩掺,可以通過特定的方法在不同狀態(tài)之間進行轉(zhuǎn)換。那么接下來研儒,我們通過代碼独令,具體來看看這些個狀態(tài)是怎么形成的。
1逸月、新建
新建狀態(tài)最為簡單遍膜,創(chuàng)建一個線程后瓢颅,尚未啟動的時候就處于此種狀態(tài)。
public static void main(String[] args) {
Thread thread = new Thread("新建線程");
System.out.println("線程狀態(tài):"+thread.getState());
}
-- 輸出:線程狀態(tài):NEW
2翰意、運行
可運行線程的狀態(tài),當我們調(diào)用了start()
方法醒第,線程正在Java虛擬機中執(zhí)行稠曼,但它可能正在等待來自操作系統(tǒng)(如處理器)的其他資源客年。
所以,這里實際上包含了兩種狀態(tài):Running 和 Ready
司恳,統(tǒng)稱為 Runnable
绍傲。這是為什么呢唧取?
這里涉及到一個Java線程調(diào)度的問題:
線程調(diào)度,是指系統(tǒng)為線程分配處理器使用權(quán)的過程邢享。調(diào)度主要方式有兩種淡诗,協(xié)同式線程調(diào)度和搶占式線程調(diào)度韩容。
- 協(xié)同式線程調(diào)度
線程的執(zhí)行時間由線程本身來控制,線程把自己的工作執(zhí)行完畢之后插爹,要主動通知系統(tǒng)切換到另外一個線程上去请梢。
- 搶占式線程調(diào)度
每個線程將由系統(tǒng)來自動分配執(zhí)行時間毅弧,線程的切換不由線程本身來決定,是基于CPU時間分片的方式寸宵。
它們孰優(yōu)孰劣,不在本文討論范圍之內(nèi)巫员。我們只需要知道疏遏,Java使用的線程調(diào)度方式就是搶占式調(diào)度救军。
通常倘零,這個時間分片是很小的呈驶,可能只有幾毫秒或幾十毫秒。所以司致,線程的實際狀態(tài)可能會在Running 和 Ready
狀態(tài)之間不斷變化脂矫。所以霉晕,再去區(qū)分它們意義不大。
那么拄轻,我們再多想一下伟葫,如果Java線程調(diào)度方式是協(xié)同式調(diào)度筏养,也許再去區(qū)分這兩個狀態(tài)就很有必要了撼玄。
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (;;){}
});
thread.start();
System.out.println("線程狀態(tài):"+thread.getState());
}
-- 輸出:線程狀態(tài):RUNNABLE
簡單來看,上面的代碼就使線程處于Runnable
狀態(tài)盏浙。但值得我們注意的是,如果一個線程在等待阻塞I/O的操作時竹海,它的狀態(tài)也是Runnable
的丐黄。
我們來看兩個經(jīng)典阻塞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);
都是阻塞式方法蕉斜。
它們一個在等待客戶端的連接;一個在等待數(shù)據(jù)的到來机错。但是父腕,這兩個線程的狀態(tài)卻是 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 狀態(tài)下的線程尖奔,正在 Java 虛擬機中執(zhí)行穷当,但它可能正在等待來自操作系統(tǒng)(如處理器)的其他資源
馁菜。
不管是CPU、網(wǎng)卡還是硬盤峭火,這些都是操作系統(tǒng)的資源而已卖丸。當進行阻塞式的IO操作時纺且,或許底層的操作系統(tǒng)線程確實處在阻塞狀態(tài),但在這里我們的 Java 虛擬機線程的狀態(tài)還是 Runnable
稍浆。
不要小看這個問題载碌,很具有迷惑性。有些面試官如果問到衅枫,如果一個線程正在進行阻塞式 I/O 操作時嫁艇,它處于什么狀態(tài)?是Blocked還是Waiting弦撩?
那這時候裳仆,我們就要義正言辭的告訴他:親,都不是哦~
3孤钦、無限期等待
處于無限期等待狀態(tài)下的線程纯丸,不會被分配處理器執(zhí)行時間偏形,除非其他線程顯式的喚醒它。
最簡單的場景就是調(diào)用了 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();
}
-- 輸出:線程狀態(tài):WAITING
此時這個線程就處于無限期等待狀態(tài)俊扭,除非有別的線程顯式的調(diào)用object.notifyAll();
來喚醒它。
然后坠陈,就是Thread.join()
方法萨惑,當主線程調(diào)用了此方法,就必須等待子線程結(jié)束之后才能繼續(xù)進行仇矾。
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("線程狀態(tài):"+thread.getState());
}
//輸出:線程狀態(tài):WAITING
如上代碼庸蔼,在主線程 mainThread
中調(diào)用了子線程的join()
方法,那么主線程就要等待子線程結(jié)束運行贮匕。所以此時主線程mainThread
的狀態(tài)就是無限期等待姐仅。
多說一句,其實join()
方法內(nèi)部刻盐,調(diào)用的也是Object.wait()
掏膏。
最后,我們說說LockSupport.park()
方法敦锌,它同樣會使線程進入無限期等待狀態(tài)馒疹。也許有的朋友對它很陌生,沒有用過乙墙,我們來看一個阻塞隊列的例子颖变。
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();
}
如上代碼生均,往往我們會通過阻塞隊列的方式來做生產(chǎn)者-消費者模型的代碼。
這里悼做,ArrayBlockingQueue
長度為1疯特,當我們第二次往里面添加數(shù)據(jù)的時候,發(fā)現(xiàn)隊列已滿肛走,線程就會等待這里漓雅,它的源碼里面正是調(diào)用了LockSupport.park()
。
同樣的朽色,這里也比較具有迷惑性邻吞,我來問你:阻塞隊列中,如果隊列為空或者隊列已滿葫男,這時候執(zhí)行take或者put操作的時候抱冷,線程的狀態(tài)是 Blocked 嗎?
那這時候梢褐,我們需要謹記這里的線程狀態(tài)還是 WAITING
旺遮。它們之間的區(qū)別和聯(lián)系,我們后文再看盈咳。
4耿眉、限期等待
同樣的,處于限期等待狀態(tài)下的線程鱼响,也不會被分配處理器執(zhí)行時間鸣剪,但是它在一定時間之后可以自動的被操作系統(tǒng)喚醒。
這個跟無限期等待的區(qū)別丈积,僅僅就是有沒有帶有超時時間參數(shù)筐骇。
比如:
object.wait(3000);
thread.join(3000);
LockSupport.parkNanos(5000000L);
Thread.sleep(1000);
像這種操作,都會使線程處于限期等待的狀態(tài) TIMED_WAITING
江滨。因為Thread.sleep()
必須帶有時間參數(shù)铛纬,所以它不在無限期等待行列中。
5唬滑、阻塞
一個線程因為等待臨界區(qū)的鎖被阻塞產(chǎn)生的狀態(tài)饺鹃,也就是說,阻塞狀態(tài)的產(chǎn)生是因為它正在等待著獲取一個排它鎖间雀。
這里悔详,我們來看一個 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鎖惹挟,線程執(zhí)行茄螃。");
}
});
t2.start();
System.out.println("線程狀態(tài):"+t2.getState());
}
//輸出:線程狀態(tài):BLOCKED
我們看上面的代碼,object對象鎖一直被線程 t1 持有连锯,所以線程 t2 的狀態(tài)一直會是阻塞狀態(tài)归苍。
我們接著再來看一個鎖的例子:
public static void main(String[] args){
Lock lock = new ReentrantLock();
lock.lock();
Thread t1 = new Thread(() -> {
lock.lock();
System.out.println("已獲取lock鎖用狱,線程執(zhí)行");
lock.unlock();
});
t1.start();
System.out.println("線程狀態(tài):"+t1.getState());
}
如上代碼,我們有一個ReentrantLock
拼弃,main線程已經(jīng)持有了這個鎖夏伊,t1 線程會一直等待在lock.lock();
。
那么吻氧,此時 t1 線程的狀態(tài)是什么呢 溺忧?
其實答案是WAITING
,即無限期等待狀態(tài)盯孙。這又是為什么呢 鲁森?
原因在于,Lock
接口是Java API實現(xiàn)的鎖振惰,它的底層實現(xiàn)其實是抽象同步隊列歌溉,簡稱AQS
。
在通過lock.lock()
獲取鎖的時候骑晶,如果鎖正在被其他線程持有痛垛,那么線程會被放入AQS隊列后,阻塞掛起桶蛔。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
如果tryAcquire返回false匙头,會把當前線程放入AQS阻塞隊列
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquireQueued
方法會將當前線程放入 AQS 阻塞隊列,然后調(diào)用LockSupport.park(this);
掛起線程羽圃。
所以,這也就解釋了為什么lock.lock()
獲取鎖的時候抖剿,當前的線程狀態(tài)會是 WAITING
朽寞。
常常有人會問,synchronized和Lock
的區(qū)別斩郎,除了一般性的答案脑融,此時你也可以說一下線程狀態(tài)的差異,我猜可能很少有人會意識到這一點缩宜。
6肘迎、結(jié)束
一個線程,當它退出或已經(jīng)執(zhí)行完成的時候锻煌,就是結(jié)束狀態(tài)妓布。
public static void main(String[] args) throws Exception {
Thread thread = new Thread(() -> System.out.println("線程已執(zhí)行"));
thread.start();
Thread.sleep(1000);
System.out.println("線程狀態(tài):"+thread.getState());
}
//輸出: 線程已執(zhí)行
線程狀態(tài):TERMINATED
三、總結(jié)
本文介紹了 Java 線程的不同狀態(tài)宋梧,以及在何種情況下發(fā)生轉(zhuǎn)換匣沼。
原創(chuàng)不易,客官們點個贊再走嘛捂龄,這將是筆者持續(xù)寫作的動力~