深入理解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方法中的循環體執行完畢,該線程完成了它的使命,便進入死亡態