第四章、Java執行緒的狀態及主要轉化方法
一、
作業系統執行緒主要有以下三個狀態:
-
就緒狀態(ready):執行緒正在等待使用CPU,經排程程式呼叫之後可進入running狀態。
-
執行狀態(running):執行緒正在使用CPU。
-
等待狀態(waiting): 執行緒經過等待事件的呼叫或者正在等待其他資源(如I/O)。
// Thread.State 原始碼 public enum State { NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED; }
2.1、NEW
用New語句建立的執行緒物件處於新建狀態。此時還沒呼叫Thread例項的start()方法,此時它和其他的java物件一樣,僅僅在堆區中分配了記憶體。
public class ThreadState_Demo { public static void main(String[] args) { Thread thread = new Thread(() -> {}); Thread.State state = thread.getState(); System.out.println("state = " + state); } }//輸出NEW
從上面可以看出,只是建立了執行緒而並沒有呼叫start()方法,此時執行緒處於NEW狀態。
關於start()的兩個引申問題
-
反覆呼叫同一個執行緒的start()方法是否可行?
-
假如一個執行緒執行完畢(此時處於TERMINATED狀態),再次呼叫這個執行緒的start()方法是否可行?
public class ThreadState_Demo { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(() -> {}); Thread.State state= thread.getState(); System.out.println("state = " + state); thread.start(); Thread.sleep(5000); System.out.println(thread.getState()); thread.start(); } }
要分析這兩個問題,我們先來看看start()的原始碼:
public synchronized void start() { if (threadStatus != 0) throw new IllegalThreadStateException(); group.add(this); boolean started = false; try { start0(); started = true; } finally { try { if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { } } }
我們可以看到,在start()內部,這裡有一個threadStatus的變數。如果它不等於0,呼叫start()是會直接丟擲異常的。
我們接著往下看,有一個native的start0()
方法。這個方法裡並沒有對threadStatus的處理。到了這裡我們彷彿就拿這個threadStatus沒轍了,我們通過debug的方式再看一下:
@Test public void testStartMethod() { Thread thread = new Thread(() -> {}); thread.start(); // 第一次呼叫 thread.start(); // 第二次呼叫 }
我是在start()方法內部的最開始打的斷點,敘述下在我這裡打斷點看到的結果:
-
第一次呼叫時threadStatus的值是0。
-
第二次呼叫時threadStatus的值不為0。
檢視當前執行緒狀態的原始碼:
// Thread.getState方法原始碼: public State getState() { // get current thread state return sun.misc.VM.toThreadState(threadStatus); } // sun.misc.VM 原始碼: public static State toThreadState(int var0) { if ((var0 & 4) != 0) { return State.RUNNABLE; } else if ((var0 & 1024) != 0) { return State.BLOCKED; } else if ((var0 & 16) != 0) { return State.WAITING; } else if ((var0 & 32) != 0) { return State.TIMED_WAITING; } else if ((var0 & 2) != 0) { return State.TERMINATED; } else { return (var0 & 1) == 0 ? State.NEW : State.RUNNABLE; } }
所以,我們結合上面的原始碼可以得到引申的兩個問題的結果:
兩個問題的答案都是不可行,在呼叫一次start()之後,threadStatus的值會改變(threadStatus !=0),此時再次呼叫start()方法會丟擲IllegalThreadStateException異常。
比如,threadStatus為2代表當前執行緒狀態為TERMINATED。
2.2、RUNNABLE
表示當前執行緒正在執行中。處於RUNNABLE狀態的執行緒在Java虛擬機器中執行,也有可能在等待CPU分配資源。此時,虛擬機器會為他建立方法呼叫棧和程式計數器。處於這種狀態的執行緒位於可執行池中,等待CPU的使用權。
Java中執行緒的RUNNABLE狀態
看了作業系統執行緒的幾個狀態之後我們來看看Thread原始碼裡對RUNNABLE狀態的定義:
2.3、BLOCKED
阻塞狀態。處於BLOCKED狀態的執行緒正等待鎖的釋放以進入同步區。
我們用BLOCKED狀態舉個生活中的例子:
等待狀態。處於等待狀態的執行緒變成RUNNABLE狀態需要其他執行緒喚醒。
呼叫如下3個方法會使執行緒進入等待狀態:
//使當前執行緒處於等待狀態直到另一個執行緒喚醒它; Object.wait(): //等待執行緒執行完畢,底層呼叫的是Object例項的wait方法; Thread.join(): //除非獲得呼叫許可,否則禁用當前執行緒進行執行緒排程。 LockSupport.park():
超時等待狀態。執行緒等待一個具體的時間,時間到後會被自動喚醒。
呼叫如下方法會使執行緒進入超時等待狀態:
//使當前執行緒睡眠指定時間; Thread.sleep(long millis) //執行緒休眠指定時間,等待期間可以通過notify()/notifyAll()喚醒; Object.wait(long timeout) //等待當前執行緒最多執行millis毫秒,如果millis為0,則會一直執行; Thread.join(long millis) //除非獲得呼叫許可,否則禁用當前執行緒進行執行緒排程指定時間; LockSupport.parkNanos(long nanos) //同上,也是禁止執行緒進行排程指定時間; LockSupport.parkUntil(long deadline)
我們繼續延續上面的例子來解釋一下TIMED_WAITING狀態:
突然間想起你的同事叫你等他一起,他說讓你等他十分鐘他改個bug。
好吧,你說那你就等等吧,你就離開了視窗。很快十分鐘過去了,你見他還沒來,你想都等了這麼久了還不來,那你還是先去吃飯好了。
這時你還是執行緒t1,你改bug的同事是執行緒t2。t2讓t1等待了指定時間,此時t1等待期間就屬於TIMED_WATING狀態。
終止狀態。此時執行緒已執行完畢。
三、
@Test public void blockedTest() { Thread a = new Thread(new Runnable() { @Override public void run() { testMethod(); } }, "a"); Thread b = new Thread(new Runnable() { @Override public void run() { testMethod(); } }, "b"); a.start(); b.start(); System.out.println(a.getName() + ":" + a.getState()); // 輸出? System.out.println(b.getName() + ":" + b.getState()); // 輸出? } // 同步方法爭奪鎖 private synchronized void testMethod() { try { Thread.sleep(2000L); } catch (InterruptedException e) { e.printStackTrace(); } }
初看之下,大家可能會覺得執行緒a會先呼叫同步方法,同步方法內又呼叫了Thread.sleep()方法,必然會輸出TIMED_WAITING,而執行緒b因為等待執行緒a釋放鎖所以必然會輸出BLOCKED。
其實不然,有兩點需要值得大家注意,一是在測試方法blockedTest()內還有一個main執行緒,二是
測試方法的main執行緒只保證了a,b兩個執行緒呼叫start()方法(轉化為RUNNABLE狀態),如果CPU執行效率高一點,還沒等兩個執行緒真正開始爭奪鎖,就已經列印此時兩個執行緒的狀態(RUNNABLE)了。
當然,如果CPU執行效率低一點,其中某個執行緒也是可能打印出BLOCKED狀態的(此時兩個執行緒已經開始爭奪鎖了)。
這時你可能又會問了,要是我想要打印出BLOCKED狀態我該怎麼處理呢?BLOCKED狀態的產生需要兩個執行緒爭奪鎖才行。那我們處理下測試方法裡的main執行緒就可以了,讓它“休息一會兒”,呼叫一下Thread.sleep()
方法。
這裡需要注意的是main執行緒休息的時間,要保證線上程爭奪鎖的時間內,不要等到前一個執行緒鎖都釋放了你再去爭奪鎖,此時還是得不到BLOCKED狀態的。
我們把上面的測試方法blockedTest()改動一下:
public void blockedTest() throws InterruptedException { ······ a.start(); Thread.sleep(1000L); // 需要注意這裡main執行緒休眠了1000毫秒,而testMethod()裡休眠了2000毫秒 b.start(); System.out.println(a.getName() + ":" + a.getState()); // 輸出? System.out.println(b.getName() + ":" + b.getState()); // 輸出? }
在這個例子中兩個執行緒的狀態轉換如下
-
a的狀態轉換過程:RUNNABLE(
a.start()
) -> TIMED_WATING(Thread.sleep()
)->RUNABLE(sleep()時間到)->BLOCKED(未搶到鎖) -> TERMINATED -
b的狀態轉換過程:RUNNABLE(
b.start()
) -> BLOCKED(未搶到鎖) ->TERMINATED
斜體表示可能出現的狀態, 大家可以在自己的電腦上多試幾次看看輸出。同樣,這裡的輸出也可能有多鍾結果。
3.2、WAITING狀態與RUNNABLE狀態的轉換
根據轉換圖我們知道有3個方法可以使執行緒從RUNNABLE狀態轉為WAITING狀態。我們主要介紹下Object.wait()和Thread.join()。
Object.wait()
呼叫wait()方法前執行緒必須持有物件的鎖。
執行緒呼叫wait()方法時,會釋放當前的鎖,直到有其他執行緒呼叫notify()/notifyAll()方法喚醒等待鎖的執行緒。
需要注意的是,其他執行緒呼叫notify()方法只會喚醒單個等待鎖的執行緒,如有有多個執行緒都在等待這個鎖的話不一定會喚醒到之前呼叫wait()方法的執行緒。
同樣,呼叫notifyAll()方法喚醒所有等待鎖的執行緒之後,也不一定會馬上把時間片分給剛才放棄鎖的那個執行緒,具體要看系統的排程。
Thread.join()
呼叫join()方法不會釋放鎖,會一直等待當前執行緒執行完畢(轉換為TERMINATED狀態)。
我們再把上面的例子執行緒啟動那裡改變一下:
public void blockedTest() { ······ a.start(); a.join(); b.start(); System.out.println(a.getName() + ":" + a.getState()); // 輸出 TERMINATED System.out.println(b.getName() + ":" + b.getState()); }
要是沒有呼叫join方法,main執行緒不管a執行緒是否執行完畢都會繼續往下走。
a執行緒啟動之後馬上呼叫了join方法,這裡main執行緒就會等到a執行緒執行完畢,所以這裡a執行緒列印的狀態固定是TERMINATED。
至於b執行緒的狀態,有可能列印RUNNABLE(尚未進入同步方法),也有可能列印TIMED_WAITING(進入了同步方法)。
TIMED_WAITING與WAITING狀態類似,只是TIMED_WAITING狀態等待的時間是指定的。
Thread.sleep(long)
Object.wait(long)
wait(long)方法使執行緒進入TIMED_WAITING狀態。這裡的wait(long)方法與無參方法wait()相同的地方是,都可以通過其他執行緒呼叫notify()或notifyAll()方法來喚醒。
不同的地方是,有參方法wait(long)就算其他執行緒不來喚醒它,經過指定時間long之後它會自動喚醒,擁有去爭奪鎖的資格。
Thread.join(long)
join(long)使當前執行緒執行指定時間,並且使執行緒進入TIMED_WAITING狀態。
我們再來改一改剛才的示例:
public void blockedTest() { ······ a.start(); a.join(1000L); b.start(); System.out.println(a.getName() + ":" + a.getState()); // 輸出 TIEMD_WAITING System.out.println(b.getName() + ":" + b.getState()); }這裡呼叫a.join(1000L),因為是指定了具體a執行緒執行的時間的,並且執行時間是小於a執行緒sleep的時間,所以a執行緒狀態輸出TIMED_WAITING。
3.4、執行緒中斷
在某些情況下,我們線上程啟動後發現並不需要它繼續執行下去時,需要中斷執行緒。目前在Java裡還沒有安全直接的方法來停止執行緒,但是Java提供了執行緒中斷機制來處理需要中斷執行緒的情況。
執行緒中斷機制是一種協作機制。需要注意,通過中斷操作並不能直接終止一個執行緒,而是通知需要被中斷的執行緒自行處理。
簡單介紹下Thread類裡提供的關於執行緒中斷的幾個方法:
//中斷執行緒。這裡的中斷執行緒並不會立即停止執行緒,而是設定執行緒的中斷狀態為true(預設是flase); Thread.interrupt() //測試當前執行緒是否被中斷。執行緒的中斷狀態受這個方法的影響,意思是呼叫一次使執行緒中斷狀態設定為true,連續呼叫兩次會使得這個執行緒的中斷狀態重新轉為false; Thread.interrupted() //測試當前執行緒是否被中斷。與上面方法不同的是呼叫這個方法並不會影響執行緒的中斷狀態。 Thread.isInterrupted()
線上程中斷機制裡,當其他執行緒通知需要被中斷的執行緒後,執行緒中斷的狀態被設定為true,但是具體被要求中斷的執行緒要怎麼處理,完全由被中斷執行緒自己而定,可以在合適的實際處理中斷請求,也可以完全不處理繼續執行下去。