深入理解Java執行緒狀態
- 2019 年 10 月 4 日
- 筆記
0 執行緒狀態概述
分類
6個狀態定義: java.lang.Thread.State
- New: 尚未啟動的執行緒的執行緒狀態。
- Runnable: 可運行執行緒的執行緒狀態,等待CPU調度。
- Blocked: 執行緒阻塞等待監視器鎖定的執行緒狀態。 處於synchronized同步程式碼塊或方法中被阻塞。
- Waiting: 等待執行緒的執行緒狀態。下 列不帶超時的方式: Object.wait、Thread.join、 LockSupport.park
- Timed Waiting:具有指定等待時間的等待執行緒的執行緒狀態。下 列帶超時的方式: Thread.sleep、0bject.wait、 Thread.join、 LockSupport.parkNanos、 LockSupport.parkUntil
- Terminated: 終止執行緒的執行緒狀態。執行緒正常完成執行或者出現異常。
流程圖


1 NEW

實現Runnable介面和繼承Thread可以得到一個執行緒類,new一個實例出來,執行緒就進入了初始狀態
執行緒還是沒有開始執行
有狀態了,那肯定是已經創建好執行緒對象了(如果對象都沒有,何來狀態這說), 問題的焦點就在於還沒有開始執行,當調用執行緒的start()方法時,執行緒不一定會馬上執行,因為Java執行緒是映射到作業系統的執行緒執行,此時可能還需要等作業系統調度,但此時該執行緒的狀態已經為RUNNABLE
2 RUNNABLE

只是說你有資格運行,調度程式沒有挑選到你,你就永遠是可運行狀態。
2.1條件
- 調用start(),進入可運行態
- 當前執行緒sleep()結束,其他執行緒join()結束,等待用戶輸入完畢,某個執行緒拿到對象鎖,這些執行緒也將進入可運行狀態
- 當前執行緒時間片用完,調用當前執行緒的yield()方法,當前執行緒進入可運行狀態
- 鎖池裡的執行緒拿到對象鎖後,進入可運行狀態
- 正在執行執行緒必屬於此態
這個狀態是最有爭議的,注釋中說了,它表示執行緒在JVM層面是執行的,但在作業系統層面不一定,它舉例是CPU,毫無疑問CPU是一個作業系統資源,但這也就意味著在等作業系統其他資源的時候,執行緒也會是這個狀態
這裡就有一個關鍵點IO阻塞算是等作業系統的資源? 3 BLOCKED

被掛起,執行緒因為某種原因放棄了cpu timeslice,暫時停止運行。
3.1條件
- 當前執行緒調用Thread.sleep(),進入阻塞態
- 運行在當前執行緒里的其它執行緒調用join(),當前執行緒進入阻塞態。
- 等待用戶輸入的時候,當前執行緒進入阻塞態。
3.2 分類
- 等待阻塞 運行的執行緒執行o.wait()方法,JVM會把該執行緒放入等待隊列(waitting queue)中
- 同步阻塞 運行的執行緒在獲取對象的同步鎖時,若該同步鎖被別的執行緒佔用,則JVM會把該執行緒放入鎖池(lock pool)中
- 其他阻塞 運行的執行緒執行Thread.sleep(long ms)或t.join()方法,或者發出了I/O請求時,JVM會把該執行緒置為阻塞狀態 當sleep()狀態超時、join()等待執行緒終止或者超時、或者I/O處理完畢時,執行緒重新轉入可運行(runnable)狀態
執行緒在阻塞等待monitor lock(監視器鎖) 一個執行緒在進入synchronized修飾的臨界區的時候,或者在synchronized臨界區中調用Object.wait然後被喚醒重新進入synchronized臨界區都對應該態。
結合上面RUNNABLE的分析,也就是I/O阻塞不會進入BLOCKED狀態,只有synchronized會導致執行緒進入該狀態
關於BLOCKED狀態,注釋里只提到一種情況就是進入synchronized聲明的臨界區時會導致,這個也很好理解,synchronized是JVM自己控制的,所以這個阻塞事件它自己能夠知道(對比理解上面的作業系統層面)。
interrupt()是無法喚醒的!只是做個標記而已!
4 等待

執行緒擁有對象鎖後進入到相應的程式碼區後,調用相應的「鎖對象」的wait()後產生的一種結果
- 變相的實現 LockSupport.park() LockSupport parkNanos( ) LockSupport parkUntil( ) Thread join( )
它們也是在等待另一個對象事件的發生,也就是描述了等待的意思。
BLOCKED狀態也是等待的意思,有什麼關係與區別呢?
- BLOCKED是虛擬機認為程式還不能進入某個區域,因為同時進去就會有問題,這是一塊臨界區
- wait()的先決條件是要進入臨界區,也就是執行緒已經拿到了「門票」,自己可能進去做了一些事情,但此時通過判定某些業務上的參數(由具體業務決定),發現還有一些其他配合的資源沒有準備充分,那麼自己就等等再做其他的事情
有一個非常典型的案例就是通過wait()和notify()完成生產者/消費者模型 當生產者生產過快,發現倉庫滿了,即消費者還沒有把東西拿走(空位資源還沒準備好) 時,生產者就等待有空位再做事情,消費者拿走東西時會發出「有空位了」的消息,那麼生產者就又開始工作了 反過來也是一樣,當消費者消費過快發現沒有存貨時,消費者也會等存貨到來,生產者生產出內容後發出「有存貨了」的消息,消費者就又來搶東西了。
在這種狀態下,如果發生了對該執行緒的interrupt()是有用的,處於該狀態的執行緒內部會拋出一個InerruptedException 這個異常應當在run()裡面捕獲,使得run()正常地執行完成。當然在run()內部捕獲異常後,還可以讓執行緒繼續運行,這完全是根據具體的應用場景來決定的。
在這種狀態下,如果某執行緒對該鎖對象做了notify(),那麼將從等待池中喚醒一個執行緒重新恢復到RUNNABLE 除notify()外,還有一個notifyAll() ,前者是 喚醒一個處於WAITING的執行緒,而後者是喚醒所有的執行緒。
Object.wait()是否需要死等呢?

不是,除中斷外,它還有兩個重構方法
- Object.wait(int timeout),傳入的timeout 參數是超時的毫秒值,超過這個值後會自動喚醒,繼續做下面的操作(不會拋出InterruptedException ,但是並不意味著我們不去捕獲,因為不排除其他執行緒會對它做interrup())。
- Object.wait(int timeout,int nanos) 這是一個更精確的超時設置,理論上可以精確到納秒,這個納秒值可接受的範圍是0~999999 (因為100000onS 等於1ms)。
同樣的 LockSupport park( ) LockSupport.parkNanos( ) LockSupport.parkUntil( ) Thread.join() 這些方法都會有類似的重構方法來設置超時,達到類似的目的,不過此時的狀態不再是WAITING,而是TIMED.WAITING
通常寫程式碼的人肯定不想讓程式死掉,但是又希望通過這些等待、通知的方式來實現某些平衡,這樣就不得不去嘗試採用「超時+重試+失敗告知」等方式來達到目的。
TIMED _WAITING

當調用Thread.sleep()時,相當於使用某個時間資源作為鎖對象,進而達到等待的目的,當時間達到時觸發執行緒回到工作狀態。
TERM_INATED

這個執行緒對象也許是活的,但是,它已經不是一個單獨執行的執行緒,在一個死去的執行緒上調用start()方法,會拋java.lang.IllegalThreadStateException. 執行緒run()、main() 方法執行結束,或者因異常退出了run()方法,則該執行緒結束生命周期。死亡的執行緒不可再次復生。 run()走完了,執行緒就處於這種狀態。其實這只是Java 語言級別的一種狀態,在作業系統內部可能已經註銷了相應的執行緒,或者將它復用給其他需要使用執行緒的請求,而在Java語言級別只是通過Java 程式碼看到的執行緒狀態而已。
為什麼wait( )和notify( )必須要使用synchronized
如果不用就會報ilegalMonitorStateException 常見的寫法如下:
synchronized(Object){ object.wait() ;//object.notify() ; } synchronized(this){ this.wait(); } synchronized fun( ){ this.wait();//this.notify(); }
wait()和notify()`是基於對象存在的。
- 那為什麼要基於對象存在呢? 既然要等,就要考慮等什麼,這裡等待的就是一個對象發出的訊號,所以要基於對象而存在。 不用對象也可以實現,比如suspend()/resume()就不需要,但是它們是反面教材,表面上簡單,但是處處都是問題
理解基於對象的這個道理後,目前認為它調用的方式只能是Object.wait(),這樣才能和對象掛鉤。但這些東西還與問題「wait()/notify() 為什麼必須要使用synchronized" 沒有 半點關係,或者說與對象扯上關係,為什麼非要用鎖呢?
既然是基於對象的,因此它不得不用一個數據結構來存放這些等 待的執行緒,而且這個數據結構應當是與該對象綁定的(通過查看C++程式碼,發現該數據結構為一個雙向鏈表),此時在這個對象上可能同時有多個執行緒調用wait()/notify(),在向這個對象所對應的雙向鏈表中寫入、刪除數據時,依然存在並發的問題,理論上 也需要一個鎖來控制。在JVM 內核源碼中並沒有發現任何自己用鎖來控制寫入的動作,只是通過檢查當前執行緒是否為對象的OWNER 來判定是否要拋出相應的異常。由此可見它希望該動作由Java 程式這個抽象層次來控制,它為什麼不想去自己控制鎖呢? 因為有些時候更低抽象層次的鎖未必是好事,因為這樣的請求對於外部可能是反覆循環地去徵用,或者這些程式碼還可能在其他地方復用,也許將它粗粒度化會更好一些,而且這樣的代在寫在Java 程式中本身也會更加清晰,更加容易看到相互之間的關係。
interrupt()操作只對處於WAITING 和TIME_WAITING 狀態的執行緒有用,讓它們]產生實質性的異常拋出。 在通常情況下,如果執行緒處於運行中狀態,也不會讓它中斷,如果中斷是成立的,可能會導致正常的業務運行出現問題。另外,如果不想用強制手段,就得為每條程式碼的運行設立檢查,但是這個動作很麻煩,JVM 不願意做這件事情,它做interruptl )僅僅是打一個標記,此時程式中通過isInterrupt()方法能夠判定是否被發起過中斷操作,如果被中斷了,那麼如何處理程式就是設計上的事情了。
舉個例子,如果程式碼運行是一個死循環,那麼在循環中可以這樣做:
while(true) { if (Thread.currentThread.isInterrupt()) { //可以做類似的break、return,拋出InterruptedExcept ion 達到某種目的,這完全由自己決定 //如拋出異常,通常包裝一層try catch 異常處理,進一步做處理,如退出run 方法或什麼也不做 } }
這太麻煩了,為什麼不可以自動呢? 可以通過一些生活的溝通方式來理解一下: 當你發現門外面有人呼叫你時,你自己是否搭理他是你的事情,這是一種有「愛」的溝通方式,反之是暴力地破門而入,把你強制「抓」出去的方式。
在JDK 1.6 及以後的版本中,可以使用執行緒的interrupted( )

判定執行緒是否已經被調用過中斷方法,表面上的效果與isInterrupted()

結果一樣,不過這個方法是一個靜態方法 除此之外,更大的區別在於這個方法調用後將會重新將中斷狀態設置為false,方便於循環利用執行緒,而不是中斷後狀態就始終為true,就無法將狀態修改回來了。類似的,判定執行緒的相關方法還有isAlive()

isDaemon()


等待隊列
- 調用wait(), notify()前,必須獲得obj鎖,也就是必須寫在synchronized(obj) 程式碼段內
- 與等待隊列相關的步驟和圖
- 執行緒1獲取對象A的鎖,正在使用對象A。
- 執行緒1調用對象A的wait()方法。
- 執行緒1釋放對象A的鎖,並馬上進入等待隊列。
- 鎖池裡面的對象爭搶對象A的鎖。
- 執行緒5獲得對象A的鎖,進入synchronized塊,使用對象A。
- 執行緒5調用對象A的notifyAll()方法,喚醒所有執行緒,所有執行緒進入鎖池。|| 執行緒5調用對象A的notify()方法,喚醒一個執行緒,不知道會喚醒誰,被喚醒的那個執行緒進入鎖池。
- notifyAll()方法所在synchronized結束,執行緒5釋放對象A的鎖。
- 鎖池裡面的執行緒爭搶對象鎖,但執行緒1什麼時候能搶到就不知道了。|| 原本鎖池+第6步被喚醒的執行緒一起爭搶對象鎖。

鎖池狀態
- 當前執行緒想調用對象A的同步方法時,發現對象A的鎖被別的執行緒佔有,此時當前執行緒進入鎖池狀態。 簡言之,鎖池裡面放的都是想爭奪對象鎖的執行緒
- 當一個執行緒1被另外一個執行緒2喚醒時,1執行緒進入鎖池狀態,去爭奪對象鎖。
- 鎖池是在同步的環境下才有的概念,一個對象對應一個鎖池
幾個方法的比較
- Thread.sleep(long millis) 一定是當前執行緒調用此方法,當前執行緒進入阻塞,不釋放對象鎖,millis後執行緒自動蘇醒進入可運行態。 作用:給其它執行緒執行機會的最佳方式。
- Thread.yield() 一定是當前執行緒調用此方法,當前執行緒放棄獲取的cpu時間片,由運行狀態變會可運行狀態,讓OS再次選擇執行緒。 作用:讓相同優先順序的執行緒輪流執行,但並不保證一定會輪流執行。實際中無法保證yield()達到讓步目的,因為讓步的執行緒還有可能被執行緒調度程式再次選中。Thread.yield()不會導致阻塞。
- t.join()/t.join(long millis),當前執行緒里調用其它執行緒1的join方法,當前執行緒阻塞,但不釋放對象鎖,直到執行緒1執行完畢或者millis時間到,當前執行緒進入可運行狀態。
- obj.wait(),當前執行緒調用對象的wait()方法,當前執行緒釋放對象鎖,進入等待隊列。依靠notify()/notifyAll()喚醒或者wait(long timeout)timeout時間到自動喚醒。
- obj.notify()喚醒在此對象監視器上等待的單個執行緒,選擇是任意性的。notifyAll()喚醒在此對象監視器上等待的所有執行緒。
疑問
- 當對象鎖被某一執行緒釋放的一瞬間,鎖池裡面的哪個執行緒能獲得這個鎖?隨機?隊列FIFO?or sth else?
- 等待隊列里許許多多的執行緒都wait()在一個對象上,此時某一執行緒調用了對象的notify()方法,那喚醒的到底是哪個執行緒?隨機?隊列FIFO?or sth else?java文檔就簡單的寫了句:選擇是任意性的。