深入理解Java線程狀態轉移
目錄
前言
看到網上關於線程狀態轉移的博客,好多都沒說明白。查了很多資料,匯總一篇,希望通過這一篇,能把這些狀態轉移解釋明白,如果有什麼沒考慮到的,希望指正
轉載註明出處原文地址://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方法中的循環體執行完畢,該線程完成了它的使命,便進入死亡態