深入理解Java執行緒狀態轉移
阿新 • • 發佈:2020-04-22
目錄
- 前言
- 狀態轉移圖
- 1.0 新建態到就緒態
- 1.1 就緒態到執行態
- 1.2 執行態到就緒態
- 1.2.1 時間片用完
- 1.2.2 t1.yield() 、Thread.yield();
- 1.3 執行態到阻塞態
- 1.3.1 Thread.sleep()
- 1.3.2 t2.join()
- 1.3.3 t1等待使用者輸入,等待鍵盤響應
- 1.4 阻塞態到就緒態
- 1.5 執行態到等待佇列
- 1.6 執行態到鎖池佇列
- 1.7 等待佇列到鎖池佇列
- 1.8 鎖池佇列到就緒態
- 1.9 執行態到死亡態
前言
看到網上關於執行緒狀態轉移的部落格,好多都沒說明白。查了很多資料,彙總一篇,希望通過這一篇,能把這些狀態轉移解釋明白,如果有什麼沒考慮到的,希望指正
轉載註明出處原文地址:https://www.cnblogs.com/darope/p/12748184.html
狀態轉移圖
- 要明白執行緒轉移的詳細過程,可以先通過一張圖片,瞭解一個執行緒的生命週期中,該執行緒會處在何種狀態:
注意:單向箭頭表示不可逆
1.0 新建態到就緒態
- 概念:1. 新建態:一個執行緒被創建出來時候所處的狀態 ;2. 就緒態:執行緒呼叫start()方法後,便處於可以被作業系統排程的狀態,即就緒態。該狀態可以由三處轉化而來,新建態執行了start、執行緒阻塞結束、鎖池等待佇列中的執行緒獲得了鎖
Thread t1 = new Thread( new Runnable() { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("hello : " + i); } } } ); // t1執行start()之後,處於就緒態,作業系統此時可以分配時間片給該執行緒,讓該執行緒執行run方法體中的內容 t1.start();
- 該狀態對應狀態圖中的第一步,比較簡單,不再贅述
1.1 就緒態到執行態
- 概念:執行態:表示當前執行緒被作業系統排程,分配了時間片,執行執行緒中的run方法時的狀態。執行態只可以由就緒態的執行緒轉化而來,如果多個執行緒都處在就緒態,就等待作業系統分配
public static void main(String[] args) { // 執行緒1 Thread t1 = new Thread(() -> { for (int i = 0; i < 10; i++) { System.out.println("t1 : running"); } }); t1.start(); // 執行緒2 Thread t2 = new Thread(() -> { for (int i = 0; i < 10; i++) { System.out.println("t2 : running"); } }); t2.start(); }
- 注:可以看到t1和t2兩個執行緒都執行start()方法後,控制檯會隨機交叉列印兩個執行緒的輸出資訊,這種隨機,是作業系統隨機分配時間片的排程決定的
1.2 執行態到就緒態
1.2.1 時間片用完
- 我們知道,作業系統為了公平,不可能從就緒態裡面選擇一個,一直執行完,而是隨機切換到另外的執行緒去執行,每個執行緒分配的執行時間結束,作業系統去呼叫別的執行緒,當前剛執行結束的執行緒便由執行態重新回到就緒態,等待作業系統的再次分配。參考上一個程式碼例子,t1的執行緒執行體方法中迴圈列印100次,t2也是,但是會看到控制檯是交叉列印的,說明了這一點
1.2.2 t1.yield() 、Thread.yield();
- 概念:在t1執行緒體中呼叫t1.yield(),和Thread.yield();本質上一樣,Thread.yield()表示當前執行緒讓渡。執行緒呼叫yield()方法,會讓該執行緒重新回到就緒佇列,但是yield()讓當前執行緒回到就緒佇列後,並不能保證作業系統再次呼叫不會選擇該執行緒,所以yield()方法不能用來控制執行緒的執行順序
public static void main(String[] args) {
// 執行緒1
Thread t1 = new Thread(() -> {
Thread.yield();
for (int i = 0; i < 10; i++) {
System.out.println("t1 : running " + i);
}
});
t1.start();
// 執行緒2
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("t2 : running " + i);
}
});
t2.start();
}
- 注意:這個程式我故意把執行緒讓步yield()方法寫線上程體剛執行的時候,也就是說,每次作業系統分配給t1執行緒時間片時候,t1都會讓步。但這次的讓步不代表t1接下來的方法不會執行,也就是我讓步之後,大家再一起搶,t1又搶到了時間片,那麼t1本次時間片內便執行接下來的方法,等時間片結束,再次分配t1時間片,t1還會讓,再接著搶,搶到和搶不到都有可能。
1.3 執行態到阻塞態
- 概念:阻塞態表示當前執行緒被由於某種原因,被掛起,也就是被阻塞,正在執行的執行緒被阻塞後,即使結束阻塞狀態也回不去執行態,只能回到就緒態,等待os分配cpu資源去排程
1.3.1 Thread.sleep()
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("hello : " + i);
}
}
);
// t1執行start()之後,處於就緒態,作業系統此時可以分配時間片給該執行緒
t1.start();
}
- 注意:讓當前執行緒睡眠,該執行緒被阻塞,睡眠時間結束,該執行緒接著執行
1.3.2 t2.join()
- 當在t1中呼叫t2.join()。那麼t1會阻塞,一直等待t2執行完畢,才結束阻塞回到就緒態
- 直接看程式碼:這裡我把t1和t2抽出來當做全域性靜態變數
public class TestThread {
static Thread t1;
static Thread t2;
public static void main(String[] args) {
// 執行緒1
t1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
if(i == 50) {
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t1 : running " + i);
}
});
t1.start();
// 執行緒2
t2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
System.out.println("t2 : running " + i);
}
});
t2.start();
}
}
- 解釋:這個程式的執行結果是,首選t1,t2掙搶時間片,按系統排程,首先控制檯t1和t2都有列印自身的輸出資訊,當t1執行到i=50的時候,呼叫了t2.join()。此時控制檯會全部列印t2的資訊,一直等待t2的迴圈結束,執行體的run方法結束,再去列印t1剩下的沒執行完的迴圈
- 所以join的流程可以抽象為下面這張圖片
1.3.3 t1等待使用者輸入,等待鍵盤響應
這個很好理解,比如你就執行一個main函式的主執行緒,等待輸入時,該執行緒是不會結束的,就是處於阻塞狀態。
1.4 阻塞態到就緒態
- 1.3中所有阻塞態結束,比如sleep結束,join後t2執行結束,使用者輸入了資訊回車等。t1會結束阻塞態,但是都是回到就緒態,無法再立即回到執行態
1.5 執行態到等待佇列
這裡牽扯到物件鎖的概念
- 兩個執行緒競爭鎖,其中t1釋放鎖,也就是把所佔有的物件鎖讓出。那麼如果不主動喚醒,該執行緒一直處在等待佇列中,得不到作業系統OS的排程
- 概念:等待佇列,就是當前執行緒佔有鎖之後,主動把鎖讓出,試自身進入等待佇列。此種wait加notify可以保證執行緒執行的先後順序。notify()是通知一個等待佇列的執行緒回到鎖池佇列。notifyAll()是通知所有處在等待佇列的執行緒,都回到鎖池佇列。
- show me code:
public static void main(String[] args) {
Object o = new Object();
// 執行緒1
Thread t1 = new Thread(() -> {
synchronized (o) {
for (int i = 0; i < 10; i++) {
try {
if(i == 5) {
// 當i=5的時候,讓出物件鎖,t1進入等待佇列
// 如果沒人通知,t1一直等待,程式不會結束
o.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1 : running " + i);
}
}
});
t1.start();
// 執行緒2
Thread t2 = new Thread(() -> {
synchronized (o) {
for (int i = 0; i < 10; i++) {
System.out.println("t2 : running " + i);
}
// 這裡t2得到鎖,執行完執行緒方法之後一定要通知t1停止等待。
// 不然t1結束不了,處在一直等待通知的狀態
o.notify();
}
});
t2.start();
}
1.6 執行態到鎖池佇列
- 參考1.5的程式,在i=5之前,t1佔有該物件鎖,t2即使start()也得不到執行,原因是該物件鎖被t1佔有,t2拿不到,所以就進入鎖池佇列
1.7 等待佇列到鎖池佇列
- 參考1.5的程式,當t1wait之後,讓出物件鎖,t1進入了等待佇列,t2拿到鎖,執行完之後,呼叫notify()讓等待佇列中的t1進入鎖池佇列。
1.8 鎖池佇列到就緒態
- 參考1.5的程式,當t2結束後,通知t1進入鎖池佇列,t2由於執行結束,處在鎖池佇列中的t1可以拿到物件鎖,進入就緒態,等待作業系統的排程,從而進入執行態
1.9 執行態到死亡態
死亡態不可逆,一旦執行緒進入死亡態,就再也回不到其他狀態
- 死亡態只能由執行態進入,執行態中的執行緒。例如通過作業系統的不停排程,t1直到把整個run方法中的迴圈體執行完畢,該執行緒完成了它的使命,便進入死亡態