面試官:您知道線程的生命周期包括哪幾個階段?
應聘者:
線程的生命周期包含5個階段,包括:新建、就緒、運行、阻塞、銷毀。
-
新建:就是剛使用new方法,new出來的線程;
-
就緒:就是調用的線程的start()方法后,這時候線程處於等待CPU分配資源階段,誰先搶的CPU資源,誰開始執行;
-
運行:當就緒的線程被調度並獲得CPU資源時,便進入運行狀態,run方法定義了線程的操作和功能;
-
阻塞:在運行狀態的時候,可能因為某些原因導致運行狀態的線程變成了阻塞狀態,比如sleep()、wait()之后線程就處於了阻塞狀態,這個時候需要其他機制將處於阻塞狀態的線程喚醒,比如調用notify或者notifyAll()方法。喚醒的線程不會立刻執行run方法,它們要再次等待CPU分配資源進入運行狀態;
-
銷毀:如果線程正常執行完畢后或線程被提前強制性的終止或出現異常導致結束,那么線程就要被銷毀,釋放資源;
完整的生命周期圖如下:
新建狀態
我們來看下面一段代碼:
1
|
Thread t1 =
new
Thread();
|
這里的創建,僅僅是在JAVA的這種編程語言層面被創建,而在操作系統層面,真正的線程還沒有被創建。只有當我們調用了 start() 方法之后,該線程才會被創建出來,進入Runnable狀態。只有當我們調用了 start() 方法之后,該線程才會被創建出來
就緒狀態
調用start()方法后,JVM 進程會去創建一個新的線程,而此線程不會馬上被 CPU 調度運行,進入Running狀態,這里會有一個中間狀態,就是Runnable狀態,你可以理解為等待被 CPU 調度的狀態
1
|
t1.start()
|
用一張圖表示如下:
那么處於Runnable狀態的線程能發生哪些狀態轉變?
Runnable狀態的線程無法直接進入Blocked狀態和Terminated狀態的。只有處在Running狀態的線程,換句話說,只有獲得CPU調度執行權的線程才有資格進入Blocked狀態和Terminated狀態,Runnable狀態的線程要么能被轉換成Running狀態,要么被意外終止。
運行狀態
當CPU調度發生,並從任務隊列中選中了某個Runnable線程時,該線程會進入Running執行狀態,並且開始調用run()方法中邏輯代碼。
那么處於Running狀態的線程能發生哪些狀態轉變?
-
被轉換成Terminated狀態,比如調用 stop() 方法;
-
被轉換成Blocked狀態,比如調用了sleep, wait 方法被加入 waitSet 中;
-
被轉換成Blocked狀態,如進行 IO 阻塞操作,如查詢數據庫進入阻塞狀態;
-
被轉換成Blocked狀態,比如獲取某個鎖的釋放,而被加入該鎖的阻塞隊列中;
-
該線程的時間片用完,CPU 再次調度,進入Runnable狀態;
-
線程主動調用 yield 方法,讓出 CPU 資源,進入Runnable狀態
阻塞狀態
Blocked狀態的線程能夠發生哪些狀態改變?
-
被轉換成Terminated狀態,比如調用 stop() 方法,或者是 JVM 意外 Crash;
-
被轉換成Runnable狀態,阻塞時間結束,比如讀取到了數據庫的數據后;
-
完成了指定時間的休眠,進入到Runnable狀態;
-
正在wait中的線程,被其他線程調用notify/notifyAll方法喚醒,進入到Runnable狀態;
-
線程獲取到了想要的鎖資源,進入Runnable狀態;
-
線程在阻塞狀態下被打斷,如其他線程調用了interrupt方法,進入到Runnable狀態;
終止狀態
一旦線程進入了Terminated狀態,就意味着這個線程生命的終結,哪些情況下,線程會進入到Terminated狀態呢?
-
線程正常運行結束,生命周期結束;
-
線程運行過程中出現意外錯誤;
-
JVM 異常結束,所有的線程生命周期均被結束。
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-
通用線程模型
在很多研發當中,實際應用是基於一個理論再進行優化的。所以,在了解JVM規范中的Java線程的生命周期之前,我們可以先了解通用的線程生命周期,這有助於我們后續對JVM線程生命周期的理解。
首先,通用的線程生命周期有五種,分別是:新建狀態(NEW)、可運行狀態(RUNNABLE)、運行狀態(RUN)、休眠狀態(SLEEP)、終止狀態(TERMINATED)。生命流程如下圖所示:
- 新建狀態(NEW)。線程在此狀態,僅僅是在編程語言層面創建了此線程,而在真正的操作系統中是沒有創建的。所以,它在這個狀態下是無法獲得CPU的執行的權限的。
- 可運行狀態(RUNNABLE)。線程到達此狀態,意味着它已經被操作系統創建,該線程獲得被CPU執行的資格,但此時還沒有被CPU執行相關操作。
- 運行狀態(RUN)。線程獲得CPU的執行權限,在一個特定的時間片內執行,線程僅在這個時間片內被稱為運行狀態。
- 休眠狀態(SLEEP)。當線程調用了某個阻塞API或者等待IO操作的時候,它會釋放當前CPU的執行權限,進入休眠狀態。此時,線程沒有獲取CPU執行的資格,只有當該線程被喚醒時,線程才能進入RUNNABLE狀態。
- 終止狀態(TERMINATED)。當線程完成程序任務或者出現異常的時候,它就會進入終止狀態。一個線程的使命就此結束。
-
JVM線程模型
JVM中的線程模型對於上面的通用線程模型進行了一些特有的分類和合並,它們的類別如下:
- 新建狀態(NEW)
- 可運行/運行狀態(RUNNABLE)
- 阻塞狀態(BLOCK)
- 等待狀態(WAITING)
- 有限等待狀態(TIMED_WATING)
- 終止狀態(TERMINATED)
而JVM中的狀態結合到通用狀態中可以如下圖所示理解:
由上圖可以看出,JVM講運行中的線程和等待運行的線程歸為一類,因為JVM不關心操作系統層面的調度,所以把這兩個狀態合並了。而Block、Wating、Timed_Wating三個狀態在操作系統層面都為休眠狀態沒有區別。所以,這三種狀態都沒有獲得CPU執行的資格。
-
線程狀態的轉換
從上面的JVM線程生命周期圖分析,我來說說一個線程從新建到消亡的狀態轉變中,到底會發生什么事情。
-
從NEW到RUNNABLE
這個很簡單,線程在被顯式聲明后,在調用start()方法前,這段時間都被稱為NEW狀態。如下代碼所示
1 Runnable task = ()-> System.out.println("線程啟動"); 2 //創建一個線程,線程狀態為NEW狀態 3 Thread thread = new Thread(task);
2.從RUNNABLE到BLOCK
從RUNNABLE到BLOCK狀態的轉變只有一種途徑,那就是在有synchronized關鍵字的程序當中。當線程執行到此,沒有獲取到synchronized的隱式鎖,線程就會從RUNNABLE被阻塞為BLOCK狀態。當阻塞中的線程獲取到synchronized隱式鎖時,它又會轉變為RUNNABLE狀態。
問:當線程調用阻塞API時,它的狀態會不會改變呢? 例如我們日常說的:ServerSockt的accept()、Scanner的next()方法等。
答案是:對於JVM層面來說,調用這些方法的線程依舊在RUNNABLE狀態,因為JVM對於等待CPU資源或等待IO資源並不關心,所以把他們歸為RUNNABLE狀態。而對於操作系統層面來說,線程則屬於休眠狀態。(對於較真的同學,可以通過jstack指令查看調用阻塞API是的線程是什么狀態)
3.RUNNABLE到WATING
其中有三種場景會使線程轉換為WATING狀態:
- 在synchronized的內部,調用wait()方法。
- 調用Thread.join()方法。該方法的意思是,當一個A線程調用了B線程的join方法,那么A線程就必須等待B線程執行完畢,此時,A線程就為WATING狀態。需要注意的是,如果是B線程自己調用自己的join方法。那么就會造成自己等待自己的局面,從而使線程無限等待。
-
調用LockSupport.park()方法。這個方法看上去很陌生,但是其實jdk中的並發包中的鎖都是由它實現的。例如:我們日常中用到的lock.lock()方法,condition.await()方法,其底層都是通過調用這個方法運行的。
4.RUNNABLE到TIMED_WATING狀態
其實從字面上就可以看出,TIMED_WATING狀態與WATING狀態的差別就是TIMED_WATING會在有限的時間內等待。所以,在WATING方法中的大多數方法,只要加上一個時間參數,就會觸發TIMED_WATING這個狀態。具體的有:
1 Thread.currentThread().join(millis); 2 Thread.sleep(millis); 3 Obj.wait(timeout); 4 LockSupport.parkNanos(Object blocker, long deadline); 5 LockSupport.parkUntil(long deadline);
5.RUNNABLE到TERMINAL狀態
當線程順利的完成run()方法中的任務,就會進入TERMINAL狀態。同時,當線程拋出沒有處理異常的時候,線程同樣會變為TERMINAL狀態。那如果業務上需要我們主動的終止線程,那應該怎么做呢?
-
終止線程的正確姿勢
在以往的jdk中,它提供了一些諸如:stop()、suspend()、resume()方法,這些方法都會直接把線程關閉,不給線程任何處理的機會。這樣做的風險可想而知,所以這些方法早就已經被標記為過時方法,不推薦使用,我也沒有詳細去了解。那么,我們現在想要終止一個線程,該怎么做呢?
答案就是:通過調用thread.interrupt();方法來達到終止線程的目的。當然,並不是調用interrupt()就會關閉線程,我們通過一個圖來了解一下具體的流程是怎樣的。
如圖所示,線程狀態的不同,對於Interrupt方法的處理也不同。流程在圖中已經比較清晰,我再列出幾個重點:
- Interrupt方法僅僅是把線程是否被中斷的標識設置為true。
- 當拋出InterruptedException時會把中斷標志清除
- 被中斷的線程狀態不同,做出的響應也會不同。運行時線程需要主動檢測、等待時的異常會拋出異常(這里可以類比硬件中的中斷,相當於一個信號)。
-
總結
我們在日常開發中,一旦遇到多線程的bug,分析線程dump信息是一個非常重要的手段。而了解線程運行時的狀態,有助於在分析信息時正確的判斷線程的狀況。同樣,我們可以通過jstack命令或者Java VisualVM可視化工具來查看線程的具體信息。