深刻理解Java線程狀態轉移

前言

看到網上關於線程狀態轉移的博客,好多都沒說明白。查了不少資料,彙總一篇,但願經過這一篇,能把這些狀態轉移解釋明白,若是有什麼沒考慮到的,但願指正html

轉載註明出處原文地址:http://www.javashuo.com/article/p-qonfhpvx-dk.htmlide

狀態轉移圖

  • 要明白線程轉移的詳細過程,能夠先經過一張圖片,瞭解一個線程的生命週期中,該線程會處在何種狀態:

    注意:單向箭頭表示不可逆

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方法中的循環體執行完畢,該線程完成了它的使命,便進入死亡態
相關文章
相關標籤/搜索