java執行緒的五大狀態,阻塞狀態詳解

Life_Goes_On發表於2020-08-17

一、狀態簡介

一個執行緒的生命週期裡有五大狀態,分別是:

  1. 新生
  2. 就緒
  3. 執行
  4. 死亡
  5. 執行後可能遇到的阻塞狀態
java執行緒的五大狀態,阻塞狀態詳解

二、相關方法

2.1 新生狀態

Thread t = new Thread();

正如我們前面所說的,一個執行緒開始之後有自己的記憶體空間,這些工作空間和主記憶體進行互動,從主記憶體拷貝資料到工作空間。

當這個語句執行的時候,執行緒建立,開闢工作空間,也就是執行緒進入了新生狀態。

2.2 就緒狀態

普通情況,一旦呼叫了:

t.start();

start 方法被呼叫,執行緒立即進入了就緒狀態,表示這個執行緒具有了執行的條件,但是還沒有開始執行,這就是就緒狀態。

執行緒就緒,但不意味著立即排程執行,因為要等待CPU的排程,一般來說是進入了就緒佇列

但是還有另外三種情況,執行緒也會進入就緒狀態,四種分別是:

  1. start()方法呼叫;
  2. 本來處於阻塞狀態,後來阻塞解除;
  3. 如果執行的時候呼叫 yield() 方法,避免一個執行緒佔用資源過多,中斷一下,會讓執行緒重新進入就緒狀態。注意,如果呼叫 yield() 方法之後,沒有其他等待執行的執行緒,此執行緒就會馬上恢復執行;
  4. JVM 本身將本地執行緒切換到了其他執行緒,那麼這個執行緒就進入就緒狀態。

2.3 執行狀態

當CPU選定了一個就緒狀態的執行緒,進行執行,這時候執行緒就進入了執行狀態,執行緒真正開始執行執行緒體的具體程式碼塊,基本是 run() 方法。

注意,一定是從 就緒狀態 - > 執行狀態,不會從阻塞到執行狀態的。

2.4 阻塞狀態

阻塞狀態指的是程式碼不繼續執行,而在等待,阻塞解除後,重新進入就緒狀態。

也就是說,阻塞狀態發生肯能是執行狀態轉過去的, 執行狀態 - > 阻塞狀態,不會從就緒狀態轉過去。

阻塞的方法有四種:

  1. sleep()方法,是佔用資源在睡覺的,可以限制等待多久;
  2. wait() 方法,和 sleep() 的不同之處在於,是不佔用資源的,限制等待多久;
  3. join() 方法,加入、合併或者是插隊,這個方法阻塞執行緒到另一個執行緒完成以後再繼續執行;
  4. 有些 IO 阻塞,比如 write() 或者 read() ,因為IO方法是通過作業系統呼叫的。

上面的方法和start() 一樣,不是說呼叫了就立即阻塞了,而是看CPU。

2.5 死亡狀態

死亡狀態指的是,執行緒體的程式碼執行完畢或者中斷執行。一旦進入死亡狀態,不能再呼叫 start() 。

讓執行緒進入死亡狀態的方法是 stop() 和 destroy() 但是都不推薦使用,jdk裡也寫了已過時。

一般的做法是,線上程內,讓執行緒自身自然死亡,或者加一些程式碼,想辦法讓執行緒執行完畢。

  1. 自然死亡:這個執行緒體裡就是多少次的迴圈,幾次呼叫,執行完了就完了。
  2. 如果不能自然死亡:加一些終止變數,然後用它作為run的條件,這樣,外部呼叫的時候根據時機,把變數設定為false。

比如下面的寫法,第一種就是我們的正常寫法(雖然很簡單但是沒有用lambda表示式,主要為了和第二種對比)

/*
    終止執行緒的方法1:自然死亡
*/
public class Status implements Runnable{
    @Override
    public void run() {
        for (int i=0; i<20; i++){
            System.out.println("studying");
        }
    }

    public static void main(String[] args) {
        Status s = new Status();
        new Thread(s).start();
    }
}
/*
    終止執行緒的方法2:外部控制
*/
public class Status implements Runnable{
    //1.加入狀態變數
    private boolean flag = true;
    @Override
    //2.關聯狀態變數
    public void run() {
        while (flag){
            System.out.println("studying");
        }
    }
    //3.對外提供狀態變數改變方法
    public void terminate() {
        this.flag = false;
    }
    public static void main(String[] args) {
        Status s = new Status();//1.新生
        new Thread(s).start();//2.就緒,隨後進入執行
        //無人為阻塞
        for (int i=0; i<100000; i++){
            if (i==80000){
                s.terminate();//3.終止
                System.out.println("結束");
            }
        }
    }
}

三、阻塞狀態詳解

上面的內容,執行緒的五大狀態裡,其他四種都比較簡單,建立物件的新生態、start開始的就緒態、cpu排程之後進入的執行態,以及正常結束或者外加干預導致的死亡態。

java執行緒的五大狀態,阻塞狀態詳解

3.1 sleep()

  • sleep(時間)指定當前執行緒阻塞的毫秒數:
  • sleep存在異常:InterruptException;
  • sleep時間到了之後執行緒進入就緒狀態;
  • sleep可以模擬網路延時、倒數計時等;
  • 每一個物件都有一個無形的鎖,sleep不會釋放鎖。(也就是我們說過的,抱著資源睡覺,這個特點對比wait)

前面用執行緒模擬搶票的網路延時,已經做過示例,就是用sleep設定執行緒阻塞的時間,達到網路延時的效果,讓那個同步問題更容易顯現出來。

我們再用龜兔賽跑的例子修改一下,前面兔子和烏龜都是一樣各自跑,這次讓兔子驕傲的愛睡覺,每隔一段路就睡一段時間。

public class Racer2 implements Runnable{
    private String winner;
    @Override
    public void run() {
        for (int dis=1; dis<=100; dis++){
            String role = Thread.currentThread().getName();
            //模擬兔子睡覺
            if (dis%10==0 && role.equals("兔子")){
                try {
                    Thread.sleep(500);//睡
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(role + " 跑了 " + dis);
            //每走一步,判斷是否比賽結束
            if (gameOver(dis))break;
        }
    }

    public boolean gameOver(int dis){
        if (winner != null){
            return true;
        } else if (dis == 100){
            winner = Thread.currentThread().getName();
            System.out.println("獲勝者是 "+winner);
            return true;
        }
        return false;
    }

    public static void main(String[] args) {
        Racer2 racer = new Racer2();//1.建立實現類
        new Thread(racer,"兔子").start();//2.建立代理類並start
        new Thread(racer,"烏龜").start();
    }
}

這裡面:

Thread.sleep(500);//睡

就是讓執行緒進入阻塞狀態,並且 500 ms 後,自動進入就緒狀態,由 cpu 排程適時重新執行。

也可以利用 sleep 模擬倒數計時,不涉及多個執行緒,所以直接在主執行緒裡面,主方法裡寫內容就可以,也不用run什麼,直接呼叫sleep設定阻塞時間。

public class SleepTime {
    public static void main(String[] args) throws InterruptedException {
        int num = 10;
        while (num>=0){
            Thread.sleep(1000);
            System.out.println(num--);
        }
    }
}

這就會完成 10-0 的倒數計時。

更花哨一點,用 Dateformat 和 Date 實現時間的顯示:

public static void main(String[] args) throws InterruptedException {
    //獲取十秒後的時間,然後倒數計時
    Date endTime = new Date(System.currentTimeMillis()+1000*10);
    //干預執行緒的結束
    long end = endTime.getTime();
    DateFormat format = new SimpleDateFormat("mm:ss");
    while (true) {
        System.out.println(format.format(endTime));
        Thread.sleep(1000);
        endTime = new Date(endTime.getTime() - 1000);//-1s
        if (end-10000 > endTime.getTime()){
            break;
        }
    }
}

3.2 wait() && notify()

和 sleep() 不同,wait() 方法和 notify() 搭配,可以互相搭配,一個經典的的使用場景以及介紹是在生產者-消費者模型中:

(待更新連結)

3.3 yield()

禮讓執行緒,讓當前正在執行的執行緒暫停,不阻塞執行緒,而是直接將執行緒從執行狀態轉入就緒狀態,讓cpu排程器重新排程。

用法和 sleep 是一樣的,也是一個靜態方法,呼叫起來的效果比 sleep 弱。

這是因為,禮讓之餘,還有 cpu 的排程在影響順序,所以無法保證達到執行緒切換的效果, cpu 還是可能呼叫當前的執行緒。

3.4 join()

join 合併執行緒,待此執行緒執行完成後,再執行其他執行緒。也就是說,阻塞其他的所有執行緒,所以其實應該是插隊執行緒,所以 join 應該翻譯成加入,插入?。

不同於 sleep 和 yield 方法,join 不是靜態方法,是一個普通方法,要通過一個具體的 Thread 物件才能呼叫 join。

public class JoinDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            for (int i=0; i<100; i++){
                System.out.println("myThread is joining" + i);
            }
        });

        t.start();

        for (int i=0; i<100; i++){
            if (i == 50){
                t.join();//插隊,此時主執行緒不能執行,而要執行 t
            }
            System.out.println("main is running" + i);
        }
    }
}

執行結果可以看出來,等到主執行緒執行到 i = 50後,t 執行緒完全執行,直到結束,才繼續執行 main 執行緒。

(當然,在 i = 50 之前,是不確定的,cpu 給兩個執行緒的安排)

join 的最重要的部分,就是插隊可以保證,這個執行緒自己一定會先執行完,這是在很多地方需要的邏輯。

四、利用列舉變數監控執行緒狀態

回過頭看執行緒的狀態轉換圖:

java執行緒的五大狀態,阻塞狀態詳解

新生態、死亡態、除了阻塞內部,其他都已經進行了練習,其中,就緒態到執行態之間是不由程式設計師控制的,所以 java 給這兩個狀態了一個統一的名稱叫 Runnable(不要和Runnable介面搞混)。

java jdk 裡面對於執行緒狀態的區分:

java執行緒的五大狀態,阻塞狀態詳解
  1. NEW 對應沒有 Started 的執行緒,對應新生態;
  2. RUNNABLE,對於就緒態和執行態的合稱;
  3. BLOCKED,WAITING,TIMED_WAITING三個都是阻塞態:
    • sleep 和 join 稱為WAITING,TIMED_WAITING(設定了時間的話);
    • wait 和 IO 流阻塞稱為BLOCKED。
  4. TERMINATED 死亡態。

我們用一個 demo 觀察一下狀態的切換過程:

public class AllState {
    public static void main(String[] args) throws InterruptedException {
        //一個擁有阻塞的執行緒
        Thread t = new Thread(()->{
            for (int i=0; i<10; i++){
                try {
                    Thread.sleep(200);//阻塞
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("........I am running.........");
            }
        });

        System.out.println(t.getState());//獲取狀態,此時應該是new

        t.start();
        System.out.println(t.getState());//獲取狀態,此時已經start

        //監控阻塞,設定結束條件為監控到 t 執行緒已經變成 Terminated
        while (!t.getState().equals(Thread.State.TERMINATED)){
            Thread.sleep(200);//主執行緒每200ms監控一次執行緒 t
            System.out.println(t.getState());
        }
    }
}
java執行緒的五大狀態,阻塞狀態詳解

從輸出結果可以看到,從 開始的 new 到 runnable(start後),到有執行緒執行了 running 的runnable,到阻塞的 timed_waiting ,到恢復 runnable,到最終的結束 terminated。

Thread 還提供了執行緒數方法,可以計數,結束條件其實還可以改成對於執行緒數的判斷,因為當 t 結束後,執行緒數就只剩下主執行緒了

while (Thread.activeCount() != 1){
    System.out.println(Thread.activeCount());
    Thread.sleep(200);//主執行緒每200ms監控一次執行緒 t
    System.out.println(t.getState());
}

然而,執行起來的時候輸出顯示的是 3 個執行緒:

java執行緒的五大狀態,阻塞狀態詳解

最後 terminated 之後陷入了執行緒數是 2 的死迴圈,和預想的不一樣。。。

引入了另一個問題,搜了一下,應該是控制檯輸出,也是一個執行緒被監控的。

相關文章