1. 程式人生 > >java執行緒的五大狀態,阻塞狀態詳解

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


# 一、狀態簡介
一個執行緒的生命週期裡有五大狀態,分別是: 1. 新生 2. 就緒 3. 執行 4. 死亡 5. 執行後可能遇到的阻塞狀態

# 二、相關方法
## 2.1 新生狀態 ```java Thread t = new Thread(); ``` 正如我們前面所說的,一個執行緒開始之後有自己的記憶體空間,這些工作空間和主記憶體進行互動,從主記憶體拷貝資料到工作空間。 當這個語句執行的時候,**執行緒建立,開闢工作空間**,也就是執行緒進入了新生狀態。 ## 2.2 就緒狀態 普通情況,一旦呼叫了: ```java 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表示式,主要為了和第二種對比) ```java /* 終止執行緒的方法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(); } } ``` ```java /* 終止執行緒的方法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排程之後進入的執行態,以及正常結束或者外加干預導致的死亡態。
## 3.1 sleep() - sleep(時間)指定當前執行緒阻塞的毫秒數: - sleep存在異常:InterruptException; - sleep時間到了之後執行緒進入就緒狀態; - sleep可以模擬網路延時、倒計時等; - **每一個物件都有一個無形的鎖,sleep不會釋放鎖**。(也就是我們說過的,抱著資源睡覺,這個特點對比wait) 前面用[執行緒模擬搶票的網路延時](https://www.cnblogs.com/lifegoeson/p/13491735.html),已經做過示例,就是用sleep設定執行緒阻塞的時間,達到網路延時的效果,讓那個同步問題更容易顯現出來。 我們再用[龜兔賽跑](https://www.cnblogs.com/lifegoeson/p/13491735.html)的例子修改一下,前面兔子和烏龜都是一樣各自跑,這次讓兔子驕傲的愛睡覺,每隔一段路就睡一段時間。 ```java 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(); } } ``` 這裡面: ```java Thread.sleep(500);//睡 ``` 就是讓執行緒進入**阻塞狀態**,並且 500 ms 後,自動進入**就緒狀態**,由 cpu 排程適時重新執行。 也可以利用 sleep 模擬倒計時,不涉及多個執行緒,所以直接在主執行緒裡面,主方法裡寫內容就可以,也不用run什麼,直接呼叫sleep設定阻塞時間。 ```java 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 實現時間的顯示: ```java 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。 ```java 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 給這兩個狀態了一個統一的名稱叫 Runnable(不要和Runnable介面搞混)。 java jdk 裡面對於執行緒狀態的區分:
1. **NEW** 對應沒有 Started 的執行緒,對應新生態; 2. **RUNNABLE**,對於就緒態和執行態的合稱; 3. **BLOCKED,WAITING,TIMED_WAITING**三個都是阻塞態: - sleep 和 join 稱為WAITING,TIMED_WAITING(設定了時間的話); - wait 和 IO 流阻塞稱為BLOCKED。 6. **TERMINATED** 死亡態。 我們用一個 demo 觀察一下狀態的切換過程: ```java 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()); } } } ```
從輸出結果可以看到,從 開始的 new 到 runnable(start後),到有執行緒執行了 running 的runnable,到阻塞的 timed_waiting ,到恢復 runnable,到最終的結束 terminated。 Thread 還提供了執行緒數方法,可以計數,結束條件其實還可以改成對於執行緒數的判斷,因為當 t 結束後,**執行緒數就只剩下主執行緒了**。 ```java while (Thread.activeCount() != 1){ System.out.println(Thread.activeCount()); Thread.sleep(200);//主執行緒每200ms監控一次執行緒 t System.out.println(t.getState()); } ``` 然而,執行起來的時候輸出顯示的是 3 個執行緒:
最後 terminated 之後陷入了執行緒數是 2 的死迴圈,和預想的不一樣。。。 引入了另一個問題,搜了一下,應該是控制檯輸出,也是一個執行緒被監