1. 程式人生 > >深入理解Java執行緒狀態轉移

深入理解Java執行緒狀態轉移

目錄
  • 前言
  • 狀態轉移圖
  • 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方法中的迴圈體執行完畢,該執行緒完成了它的使命,便進入死亡態