線程是操做系統中的一個概念,支持多線程的語言都是對OS中的線程進行了封裝。要學好線程,就要搞清除它的生命週期,也就是生命週期各個節點的狀態轉換機制。不一樣的開發語言對操做系統中的線程進行了不一樣的封裝,可是對於線程的聲明週期這部分基本是相同的。下面先介紹通用的線程生命週期模型,而後詳細介紹Java中的線程生命週期以及Java生命週期中各個狀態是如何轉換的。java
上圖爲通用線程狀態轉換圖(五態模型)。編程
初始狀態多線程
線程被建立,可是還不容許分配CPU執行。這裏的建立僅僅是指在編程語言層面被建立;在OS層面尚未被建立。併發
可運行狀態編程語言
線程能夠分配CPU執行。在這種狀態下,真正的OS線程已經被成功建立,因此能夠分配CPU執行。ide
運行狀態spa
當有空閒的CPU時,OS就會將空閒CPU分配給一個處於可運行狀態的線程,被分配到CPU的線程的狀態就轉換成了運行狀態。操作系統
休眠狀態線程
運行狀態的線程若是調用一個阻塞的API(例如以阻塞方式讀文件)或者等待某個事件(例如條件變量),那麼線程的狀態就會轉到休眠狀態,此時會釋放CPU使用權,休眠狀態的線程永遠沒有機會得到CPU的使用權。當等待的事件出現了(線程被喚醒),線程就會從休眠狀態轉到可運行狀態。調試
終止狀態
程序執行完成或者出現異常就會進入此狀態。終止狀態的線程不會切換到其餘任何狀態,進入終止狀態也就意味着線程的生命週期結束了。
以上五種狀態在不一樣的編程語言中會簡化合並(C中POSIX Thread規範將初始狀態和可運行狀態合併)或者細化(Java中細化了休眠狀態)。Java中將可運行狀態和運行狀態合併了,Java虛擬機不關心這兩個狀態,把線程的調度交給了操做系統。
Java語言的線程共有六種狀態:New
(初始化狀態)、RUNNABLE
(可運行狀態/運行狀態)、BLOCKED
(阻塞狀態)、WAITING
(無時限等待)、TIMED_WAITING
(有時限等待)、TERMINATED
(終止狀態)。
在操做系統層面,Java線程中的 BLOCKED、 WAITING 、TIMED_WAITING都是休眠狀態。只要Java處於這三種狀態之一,那麼這個線程就永遠沒有CPU使用權。
下面是Java線程的狀態轉換圖:
這六種狀態之間的轉換,注意箭頭的方向,哪些狀態是能夠互轉的哪些是不能夠互轉。
只有一種場景會觸發這種轉換,即線程等待synchronized內置鎖。synchronized關鍵修飾的方法、代碼塊同一時刻只容許一個線程執行,其餘未能執行的線程則等待。這種狀況下,等待的線程就會從RUNNABLE轉換到 BLOCKED狀態。當等待的線程得到內置鎖時,就會從BLOCKED轉換到RUNNABLE狀態。
線程調用阻塞式API時,在操做系統層面線程是會轉到休眠狀態,可是在Java虛擬機層面,Java線程的狀態是不會發生變化的,會保持RUNNABLE狀態。Java虛擬機層面並不關心操做系統相關調度狀態,在它眼裏,等待CPU使用權(OS層面處於可執行狀態)和等待I/O(OS層面處於休眠狀態)沒有區別,都是在等待某個資源,因此都納入了RUNNABLE狀態。
因此,平時說Java在調用阻塞式API時,線程會阻塞,指的是操做系統線程的狀態,並非Java線程的狀態。
有三種場景會觸發這種轉換:
獲取synchronized內置鎖的線程,調用無參數的Object.wait()
方法。
當前線程調用wait()
方法會將本身阻塞,狀態就從從RUNNABLE轉到WAITING狀態。使用同一內置鎖的其餘線程可調用notifyAll()
喚醒阻塞在該鎖上的全部線程,此時被阻塞的線程狀態就會從WAITING轉到RUNNABLE狀態。
調用Thread.join()
方法。
一個線程對象thread A,當調用A.join()
的時候,執行這條語句的線程會等待thread A執行完,而等待的這個線程,其狀態就會就會從RUNNABLE轉到WAITING狀態。當thread A執行完,原來的這個等待線程就會從WAITING狀態轉到RUNNABLE狀態。
調用LockSupport.park()
方法。
Java併發包中的鎖都是基於LockSupport
對象實現的。調用LockSupport.park()
的當前線程會被阻塞,線程的狀態會從RUNNABLE轉到WAITING狀態。調用LockSupport.unpark(Thread t)
可喚醒被阻塞的目標線程,目標線程的狀態就會從WAITING轉到RUNNABLE狀態。
如下場景將會觸發這個狀態轉變:
Thread.sleep(long millis)
方法。Object.wait(long timeout)
方法;Thread.join(long millis)
方法;LockSupport.parkNanos(Object blocker, long deadline)
方法;LockSupport.parkUntil(long deadline)
方法。較與WAITING狀態觸發條件多了超時參數。
Java剛建立出來的Thread對象就是NEW狀態,而建立Thread對象主要有兩種方法。
一種是繼承Thread對象,重寫run()方法。
// 自定義線程類 class MyThread extends Thread { @Override public void run() { // 線程須要執行的代碼 ...... } } // 建立線程對象 MyThread myThread = new MyThread();
二是實現Runnable接口,重寫run()方法,並將該實現類做爲Thread對象的參數。
// 實現 Runnable 接口 class Runner implements Runnable { @Override public void run() { // 線程須要執行的代碼 ...... } } // 建立線程對象 Thread thread = new Thread(new Runner());
NEW狀態的線程是不會被操做系統調度的,所以不會執行。Java線程要執行,就必須轉換到RUNNABLE狀態。那麼如何轉到RUNNABLE狀態呢?那就須要線程啓動,即調用線程的start()
方法。
MyThread myThread = new MyThread(); // 從 NEW 狀態轉換到 RUNNABLE 狀態 myThread.start();
線程執行完 run()
方法後,會自動轉換到 TERMINATED 狀態。
若是執行 run() 方法的時候異常拋出,也會致使線程終止。有時候咱們須要強制中斷 run() 方法的執行,能夠發現Java 的 Thread 類裏面卻是有個 stop()
方法,可是該方法被標記爲 @Deprecated
,已經被棄用了。因此,正確的方式是調用 interrupt()
方法。
stop()方法和interrupt()方法的主要區別:
stop()方法會直接殺死線程。若是線程持有 ReentrantLock 鎖,被 stop() 的線程並不會自動調用 ReentrantLock 的 unlock() 去釋放鎖,那其餘線程將再也沒機會得到 ReentrantLock 鎖。這將會致使很是糟糕的結果。因此該方法已經被廢棄。
而 interrupt() 方法就比較溫柔,interrupt() 方法僅僅是通知線程,線程有機會執行一些後續操做,同時也能夠無視這個通知。
線程是如何收到interrupt通知呢?有兩種方式,一種是異常,一種是主動檢測。
異常獲取通知:
當線程 A 處於 WAITING、TIMED_WAITING 狀態時,若是其餘線程調用線程 A 的 interrupt() 方法,會使線程 A 返回到 RUNNABLE 狀態,同時線程 A 的代碼會觸發 InterruptedException
異常。
上面介紹狀態轉換時, WAITING、TIMED_WAITING 狀態的觸發條件,都是調用了 wait()、join()、sleep() 這樣的方法。 咱們看這些方法的簽名,會發現它們都會throws InterruptedException 這個異常。這個異常的觸發條件就是:其餘線程調用了該線程的 interrupt() 方法。
當線程 A 處於 RUNNABLE 狀態時,而且阻塞在java.nio.channels.InterruptibleChannel
上時,若是其餘線程調用線程 A 的 interrupt() 方法,線程 A 會觸發 java.nio.channels.ClosedByInterruptException
這個異常;而阻塞在 java.nio.channels.Selector
上時,若是其餘線程調用線程 A 的 interrupt() 方法,線程 A 的 java.nio.channels.Selector
會當即返回。(這種方式我尚未使用過暫時還不太明白,先寫將這種觸發方式寫在這裏)
主動檢測獲取通知
若是線程處於 RUNNABLE 狀態,而且沒有阻塞在某個 I/O 操做上,這時就得依賴線程 A 主動檢測中斷狀態。若是其餘線程調用線程 A 的 interrupt() 方法,那麼線程 A 能夠經過 isInterrupted() 方法,檢測是否是本身被中斷了。
線程的生命週期以及各個狀態的轉換要好好掌握,這對於調試bug仍是頗有用的。
參考: [1]極客時間專欄王寶令《Java併發編程實戰》