Java語言定義了 6 種線程狀態,在任意一個時間點中,一個線程只能只且只有其中的一種狀態,而且能夠經過特定的方法在不一樣狀態之間進行轉換。java
今天,咱們就詳細聊聊這幾種狀態,以及在什麼狀況下會發生轉換。面試
要想知道Java線程都有哪些狀態,咱們能夠直接來看 Thread
,它有一個枚舉類 State
。bash
public class Thread {
public enum State {
/**
* 新建狀態
* 建立後還沒有啓動的線程
*/
NEW,
/**
* 運行狀態
* 包括正在執行,也可能正在等待操做系統爲它分配執行時間
*/
RUNNABLE,
/**
* 阻塞狀態
* 一個線程由於等待臨界區的鎖被阻塞產生的狀態
*/
BLOCKED,
/**
* 無限期等待狀態
* 線程不會被分配處理器執行時間,須要等待其餘線程顯式喚醒
*/
WAITING,
/**
* 限期等待狀態
* 線程不會被分配處理器執行時間,但也無需等待被其餘線程顯式喚醒
* 在必定時間以後,它們會由操做系統自動喚醒
*/
TIMED_WAITING,
/**
* 結束狀態
* 線程退出或已經執行完成
*/
TERMINATED;
}
}
複製代碼
咱們說,線程狀態並不是是一成不變的,能夠經過特定的方法在不一樣狀態之間進行轉換。那麼接下來,咱們經過代碼,具體來看看這些個狀態是怎麼造成的。socket
新建狀態最爲簡單,建立一個線程後,還沒有啓動的時候就處於此種狀態。ui
public static void main(String[] args) {
Thread thread = new Thread("新建線程");
System.out.println("線程狀態:"+thread.getState());
}
-- 輸出:線程狀態:NEW
複製代碼
可運行線程的狀態,當咱們調用了start()
方法,線程正在Java虛擬機中執行,但它可能正在等待來自操做系統(如處理器)的其餘資源。this
因此,這裏實際上包含了兩種狀態:Running 和 Ready
,統稱爲 Runnable
。這是爲何呢?spa
這裏涉及到一個Java線程調度的問題:操作系統
線程調度,是指系統爲線程分配處理器使用權的過程。調度主要方式有兩種,協同式線程調度和搶佔式線程調度。.net
線程的執行時間由線程自己來控制,線程把本身的工做執行完畢以後,要主動通知系統切換到另一個線程上去。線程
每一個線程將由系統來自動分配執行時間,線程的切換不禁線程自己來決定,是基於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?
那這時候,咱們就要義正言辭的告訴他:親,都不是哦~
處於無限期等待狀態下的線程,不會被分配處理器執行時間,除非其餘線程顯式的喚醒它。
最簡單的場景就是調用了 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
。它們之間的區別和聯繫,咱們後文再看。
一樣的,處於限期等待狀態下的線程,也不會被分配處理器執行時間,可是它在必定時間以後能夠自動的被操做系統喚醒。
這個跟無限期等待的區別,僅僅就是有沒有帶有超時時間參數。
好比:
object.wait(3000);
thread.join(3000);
LockSupport.parkNanos(5000000L);
Thread.sleep(1000);
複製代碼
像這種操做,都會使線程處於限期等待的狀態 TIMED_WAITING
。由於Thread.sleep()
必須帶有時間參數,因此它不在無限期等待行列中。
一個線程由於等待臨界區的鎖被阻塞產生的狀態,也就是說,阻塞狀態的產生是由於它正在等待着獲取一個排它鎖。
這裏,咱們來看一個 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
的區別,除了通常性的答案,此時你也能夠說一下線程狀態的差別,我猜可能不多有人會意識到這一點。
一個線程,當它退出或已經執行完成的時候,就是結束狀態。
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 線程的不一樣狀態,以及在何種狀況下發生轉換。
原創不易,客官們點個贊再走嘛,這將是筆者持續寫做的動力~