一、狀態簡介
一個執行緒的生命週期裡有五大狀態,分別是:
- 新生
- 就緒
- 執行
- 死亡
- 執行後可能遇到的阻塞狀態
二、相關方法
2.1 新生狀態
Thread t = new Thread();
正如我們前面所說的,一個執行緒開始之後有自己的記憶體空間,這些工作空間和主記憶體進行互動,從主記憶體拷貝資料到工作空間。
當這個語句執行的時候,執行緒建立,開闢工作空間,也就是執行緒進入了新生狀態。
2.2 就緒狀態
普通情況,一旦呼叫了:
t.start();
start 方法被呼叫,執行緒立即進入了就緒狀態,表示這個執行緒具有了執行的條件,但是還沒有開始執行,這就是就緒狀態。
執行緒就緒,但不意味著立即排程執行,因為要等待CPU的排程,一般來說是進入了就緒佇列。
但是還有另外三種情況,執行緒也會進入就緒狀態,四種分別是:
- start()方法呼叫;
- 本來處於阻塞狀態,後來阻塞解除;
- 如果執行的時候呼叫 yield() 方法,避免一個執行緒佔用資源過多,中斷一下,會讓執行緒重新進入就緒狀態。注意,如果呼叫 yield() 方法之後,沒有其他等待執行的執行緒,此執行緒就會馬上恢復執行;
- JVM 本身將本地執行緒切換到了其他執行緒,那麼這個執行緒就進入就緒狀態。
2.3 執行狀態
當CPU選定了一個就緒狀態的執行緒,進行執行,這時候執行緒就進入了執行狀態,執行緒真正開始執行執行緒體的具體程式碼塊,基本是 run() 方法。
注意,一定是從 就緒狀態 - > 執行狀態,不會從阻塞到執行狀態的。
2.4 阻塞狀態
阻塞狀態指的是程式碼不繼續執行,而在等待,阻塞解除後,重新進入就緒狀態。
也就是說,阻塞狀態發生肯能是執行狀態轉過去的, 執行狀態 - > 阻塞狀態,不會從就緒狀態轉過去。
阻塞的方法有四種:
- sleep()方法,是佔用資源在睡覺的,可以限制等待多久;
- wait() 方法,和 sleep() 的不同之處在於,是不佔用資源的,限制等待多久;
- join() 方法,加入、合併或者是插隊,這個方法阻塞執行緒到另一個執行緒完成以後再繼續執行;
- 有些 IO 阻塞,比如 write() 或者 read() ,因為IO方法是通過作業系統呼叫的。
上面的方法和start() 一樣,不是說呼叫了就立即阻塞了,而是看CPU。
2.5 死亡狀態
死亡狀態指的是,執行緒體的程式碼執行完畢或者中斷執行。一旦進入死亡狀態,不能再呼叫 start() 。
讓執行緒進入死亡狀態的方法是 stop() 和 destroy() 但是都不推薦使用,jdk裡也寫了已過時。
一般的做法是,線上程內,讓執行緒自身自然死亡,或者加一些程式碼,想辦法讓執行緒執行完畢。
- 自然死亡:這個執行緒體裡就是多少次的迴圈,幾次呼叫,執行完了就完了。
- 如果不能自然死亡:加一些終止變數,然後用它作為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排程之後進入的執行態,以及正常結束或者外加干預導致的死亡態。
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 給這兩個狀態了一個統一的名稱叫 Runnable(不要和Runnable介面搞混)。
java jdk 裡面對於執行緒狀態的區分:
- NEW 對應沒有 Started 的執行緒,對應新生態;
- RUNNABLE,對於就緒態和執行態的合稱;
- BLOCKED,WAITING,TIMED_WAITING三個都是阻塞態:
- sleep 和 join 稱為WAITING,TIMED_WAITING(設定了時間的話);
- wait 和 IO 流阻塞稱為BLOCKED。
- 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());
}
}
}
從輸出結果可以看到,從 開始的 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 個執行緒:
最後 terminated 之後陷入了執行緒數是 2 的死迴圈,和預想的不一樣。。。
引入了另一個問題,搜了一下,應該是控制檯輸出,也是一個執行緒被監控的。