深入理解Java線程狀態

  • 2019 年 10 月 4 日
  • 筆記

0 線程狀態概述

分類

6個狀態定義: java.lang.Thread.State

  1. New: 尚未啟動的線程的線程狀態。
  2. Runnable: 可運行線程的線程狀態,等待CPU調度。
  3. Blocked: 線程阻塞等待監視器鎖定的線程狀態。 處於synchronized同步代碼塊或方法中被阻塞。
  4. Waiting: 等待線程的線程狀態。下 列不帶超時的方式: Object.wait、Thread.join、 LockSupport.park
  5. Timed Waiting:具有指定等待時間的等待線程的線程狀態。下 列帶超時的方式: Thread.sleep、0bject.wait、 Thread.join、 LockSupport.parkNanos、 LockSupport.parkUntil
  6. 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()

等待隊列

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

鎖池狀態

  1. 當前線程想調用對象A的同步方法時,發現對象A的鎖被別的線程佔有,此時當前線程進入鎖池狀態。 簡言之,鎖池裏面放的都是想爭奪對象鎖的線程
  2. 當一個線程1被另外一個線程2喚醒時,1線程進入鎖池狀態,去爭奪對象鎖。
  3. 鎖池是在同步的環境下才有的概念,一個對象對應一個鎖池

幾個方法的比較

  • 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()喚醒在此對象監視器上等待的所有線程。

疑問

  1. 當對象鎖被某一線程釋放的一瞬間,鎖池裏面的哪個線程能獲得這個鎖?隨機?隊列FIFO?or sth else?
  2. 等待隊列里許許多多的線程都wait()在一個對象上,此時某一線程調用了對象的notify()方法,那喚醒的到底是哪個線程?隨機?隊列FIFO?or sth else?java文檔就簡單的寫了句:選擇是任意性的。