Java執行緒狀態(生命週期)--一篇入魂
1.執行緒狀態(生命週期)
一個執行緒在給定的時間點只能處於一種狀態。
執行緒可以有如下6 種狀態:
New (新建立):未啟動的執行緒; Runnable (可執行):可執行的執行緒,需要等待作業系統資源; Blocked (被阻塞):等待監視器鎖而被阻塞的執行緒; Waiting (等待):等待喚醒狀態,無限期地等待另一個執行緒喚醒; Timed waiting (計時等待):在指定的等待時間內等待另一個執行緒執行操作的執行緒; Terminated (被終止):已退出的執行緒。
要確定一個執行緒的當前狀態, 可呼叫getState 方法
執行緒狀態關係圖
注意:虛線框(全大寫英文)的狀態為Java執行緒狀態。
2.操作執行緒狀態
2.1.新建立狀態(NEW)
就是例項化執行緒完成後,未啟動執行緒的狀態。
可通過三種方式建立執行緒
重寫Thread類run()方法 實現Runnable介面 實現Callable介面
一個簡單的例子概括三種方式
public class Demo { public static void main(String[] args) throws ExecutionException, InterruptedException { /** * 1.直接重寫run() 或繼承Thread類再重寫run() */ Thread thread = new Thread() { @Override public void run() { System.out.println("Thread"); } }; // 開啟執行緒 thread.start(); /** * 2.lambda、內部類或執行緒類方式實現Runnable介面,實現run()方法 * 再交給Thread 類 */ Thread runThread = new Thread(() -> { System.out.println("Runnable"); }); // 開啟執行緒 runThread.start(); /** * 3.lambda、內部類或執行緒類方式實現Callable介面,實現call()方法 * 再交給Thread 類:FutureTask本質也是Runnable實現類 */ FutureTask<String> futureTask = new FutureTask<String>(() -> { System.out.println("Callable"); return "CallableThread"; }); Thread callThread = new Thread(futureTask); // 開啟執行緒 callThread.start(); // 獲取call()方法的返回值 String s = futureTask.get(); System.out.println("call()方法的返回值:"+s); } }
不重寫 run() 或 call() 方法直接例項化Thread類建立的執行緒沒有實際意義;
只有Callable方式建立的執行緒可以獲取執行緒的返回值。
2.2.可執行狀態(RUNNABLE)
該狀態指的是執行緒例項化物件呼叫start()方法後進入的狀態。執行緒處於可以執行狀態,如果有處理器等資源,就可以執行程式。
該狀態在作業系統層面包含兩步:執行緒就緒和執行緒執行中,但在Java執行緒狀態中,這兩步都統稱為Runnable(可執行)狀態。
執行緒由就緒狀態變為執行狀態,重點就看你的執行緒有沒有搶到CPU資源(CPU時間片),誰搶到就執行,沒搶到就等。因為CPU時間片(執行時間)非常短,大概十幾毫秒,所以執行緒切換的這個時間是非常短的,就緒狀態變為執行狀態的時間也非常短,在開發時幾乎感覺不到這種狀態的變化,所以在Java中將兩者看作是一個整體,重點關注執行緒可否執行並區別於其他狀態即可,更進一步簡化執行緒的開發。如果你的程式要執行很久(比如寫個死迴圈),在一個CPU時間片內沒有執行完成,那麼你的執行緒就要搶下一次的CPU時間片,搶到了才可以繼續執行程式,沒搶到那就要繼續搶,直到執行緒中的程式執行完成。
其實這個場景應該都見到過,例如多個執行緒執行同一個程式,都將日誌列印到同一個檔案時,就會出現不同執行緒的日誌混在了一起的情況,不利於排查問題。解決這種問題常見的方法有:一是分執行緒列印日誌到不同檔案;二是將日誌資訊儲存到字串物件中,在程式的最後將日誌資訊一次性列印到檔案。第二種方式就是利用CPU的一個時間片來完成日誌資訊的列印。
注意:程式只能對新建狀態的執行緒呼叫start()方法,不要對處於非新建狀態的執行緒呼叫start() 方法,這都會引發IllegalThreadStateException異常。
2.3.被阻塞狀態(BLOCKED)
執行緒處於等待監視器鎖而被阻塞的狀態。有一個執行緒獲取了鎖未釋放,其他執行緒也來獲取,但發現獲取不到鎖也進入了被阻塞狀態。
被阻塞狀態只存在於多執行緒併發訪問下,區別於後面兩種因執行緒自己進入”等待“而導致的阻塞。
進入狀態
進入synchronized 程式碼塊/方法 未獲取到鎖
退出狀態
獲取到監視器鎖
2.4.等待喚醒狀態(WAITING)
整個流程是這樣的:執行緒在某個物件的同步方法中先獲取到物件鎖;在執行wait方法時,該執行緒將釋放物件鎖,並且該執行緒被放入到這個物件的等待佇列;等待另一個執行緒獲取到同一個物件的鎖,然後通過notify() 或 notifyAll() 方法喚醒物件等待佇列中的執行緒。
從整個流程可以知道
wait (),notify () 和 notifyAll () 方法需要線上程獲取到鎖的情況下才可以繼續執行,所以這三個方法都需要放在同步程式碼塊/方法中執行,否則報異常:java.lang.IllegalMonitorStateException。
在同步程式碼塊中,執行緒進入WAITING 狀態時,鎖會被釋放,不會導致該執行緒阻塞。反過來想下,如果鎖沒釋放,那其他執行緒就沒辦法獲取鎖,也就沒辦法喚醒它。
進入狀態
object.wait() thread.join() LockSupport.park()
退出狀態
object.notify() object.notifyall() LockSupport.unpark()
2.5.計時等待狀態(TIMED_WAITING)
一般是計時結束就會自動喚醒執行緒繼續執行後面的程式,對於Object.wait(long) 方法還可以主動通知喚醒。
注意:Thread類下的sleep() 方法可以放在任意地方執行;而wait(long) 方法和wait() 方法一樣,需要放在同步程式碼塊/方法中執行,否則報異常:java.lang.IllegalMonitorStateException。
進入狀態
Thread.sleep(long) Object.wait(long) Thread.join(long) LockSupport.parkNanos(long) LockSupport.parkNanos(Object blocker, long nanos) LockSupport.parkUntil(long) LockSupport.parkUntil(Object blocker, long deadline)
注:blocker 引數為負責此執行緒駐留的同步物件。
退出狀態
計時結束 LockSupport.unpark(Thread) object.notify() object.notifyall()
2.6.終止(TERMINATED)
執行緒執行結束
run()/call() 執行完成 stop()執行緒 錯誤或異常>>意外死亡
stop() 方法已棄用。
3.檢視執行緒的6種狀態
通過一個簡單的例子來檢視執行緒出現的6種狀態。
案例
public class Demo3 {
private static Object object ="obj";
public static void main(String[] args) throws InterruptedException {
Thread thread0 = new Thread(() -> {
try {
// 被阻塞狀態(BLOCKED)
synchronized (object){
System.out.println("thread0 進入:等待喚醒狀態(WAITING)");
object.wait();
System.out.println("thread0 被解除完成:等待喚醒狀態(WAITING)");
}
System.out.println("thread0 "+Thread.currentThread().getState());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 新建立狀態(NEW)
System.out.println(thread0.getName()+":"+thread0.getState());
Thread thread1 = new Thread(() -> {
try {
System.out.println("thread1 進入:計時等待狀態(TIMED_WAITING)");
Thread.sleep(2);
System.out.println("thread1 出來:計時等待狀態(TIMED_WAITING)");
} catch (InterruptedException e) {
e.printStackTrace();
}
// 被阻塞狀態(BLOCKED)
synchronized (object){
System.out.println("thread1 解除:等待喚醒狀態(WAITING)");
object.notify();
System.out.println("thread1 解除完成:等待喚醒狀態(WAITING)");
}
System.out.println("thread1 "+Thread.currentThread().getState());
});
// 新建立狀態(NEW)
System.out.println(thread1.getName()+":"+thread1.getState());
printState(thread0);
printState(thread1);
// 可執行狀態(RUNNABLE)
thread0.start();
// 可執行狀態(RUNNABLE)
thread1.start();
}
// 使用獨立執行緒來列印執行緒狀態
private static void printState(Thread thread) {
new Thread(()->{
while (true){
System.out.println(thread.getName()+":"+thread.getState());
if (thread.getState().equals(Thread.State.TERMINATED)){
System.out.println(thread.getName()+":"+thread.getState());
break;
}
}
}).start();
}
}
執行結果:簡化後的輸出結果
Thread-0:NEW
Thread-1:NEW
Thread-0:RUNNABLE
Thread-1:RUNNABLE
thread0 進入:等待喚醒狀態(WAITING)
Thread-1:BLOCKED
thread1 進入:計時等待狀態(TIMED_WAITING)
Thread-0:BLOCKED
Thread-0:WAITING
……
Thread-0:WAITING
Thread-1:BLOCKED
Thread-1:TIMED_WAITING
……
Thread-1:TIMED_WAITING
Thread-1:BLOCKED
……
Thread-1:BLOCKED
Thread-0:WAITING
……
Thread-0:WAITING
thread1 出來:計時等待狀態(TIMED_WAITING)
Thread-0:WAITING
Thread-1:BLOCKED
thread1 解除:等待喚醒狀態(WAITING)
Thread-1:BLOCKED
Thread-0:WAITING
Thread-0:BLOCKED
thread1 解除完成:等待喚醒狀態(WAITING)
Thread-1:BLOCKED
thread1 RUNNABLE
Thread-0:BLOCKED
Thread-1:TERMINATED
thread0 被解除完成:等待喚醒狀態(WAITING)
Thread-0:BLOCKED
thread0 RUNNABLE
Thread-0:TERMINATED
最終的執行結果如圖。
注意:因為案例中使用了獨立執行緒來列印不同執行緒的狀態,會出現狀態列印稍微延遲的情況。
更多優質文章,請關注WX公眾號:Java全棧佈道師