天天看點

一文徹底搞懂java線程生命周期

前言

線程也有自己的“生老病死”,專業的說法就是生命周期,而掌握線程的生命周期,能幫我們快速分析和定位線程相關的一些問題。比如說,當我們列印線程的堆棧,發現某線程一直處于BOCKED狀态,我們就可以以此推測是不是鎖沒有釋放導緻的,然後根據堆棧資訊定位到具體的方法,進一步排查問題。

通用線程生命周期模型

在講java的線程生命周期之前,需要先了解一下通用的線程生命周期模型,因為各種語言包括中線程的本質其實就是作業系統的線程,隻是不同語言進行了不同程度的封裝。通用的生命周期模型主要包含了五個狀态節點,簡稱為"五态模型",其狀态圖如下:

一文徹底搞懂java線程生命周期

<u>初始狀态</u>:初始狀态是指在程式設計語言層面已經将線程建立好了,不過此時在作業系統層面,線程還沒有真正建立,這時的線程還不具備執行的能力。

<u>可運作狀态</u>:此時的線程已經具備執行的能力了,處于排程隊列中,就差被選中配置設定到cpu上執行。

<u>運作狀态</u>:線程被排程器選中,拿到時間片,到cpu上執行。

<u>休眠狀态</u>:線程阻塞或休眠,會讓出cpu,處于休眠狀态。當線程解除休眠狀态後,會再次進入可運作狀态,接受線程排程器排程。

<u>終止狀态</u>:線程被異常中斷或結束運作,進入終結狀态。

java線程生命周期

首先,我們看下,jdk的Thread類對線程狀态的定義:

可以看出這個枚舉類,一共定義了6個狀态,看起來還挺複雜的,不要慌,聽我講完後,你會發現其實挺簡單的。對于這種狀态流程類的事物,有一種非常好的處理技巧,就是先找主幹,再看分支,這個對于了解java生命周期也同樣适用。

首先,我們看下主幹流程。假設有一個線程,從它建立到終止,執行過程中沒有遇到任何阻塞或等待,非常順利,那麼他的生命周期圖就是下面這樣直溜溜的:

<img src="https://gitee.com/thomasChant/drawing-bed/raw/master/image-20210401220546262.png" alt="image-20210401220546262" style="zoom:100%;" />

可以看到,主幹流程中就三個狀态:初始、運作、終止,詳細講解下:

調用線程構造方法如new Thread(),建立一個線程,此時線程還不具備執行的能力,隻是在語言層面已經建立好了。我們可以調用線程的getState()方法擷取到線程狀态。

調用線程的start()方法啟動線程,線程狀态變為RUNNABLE(運作狀态),此時線程才真正在作業系統層面被建立,并加入排程隊列中,一旦配置設定到時間片後就會執行。至于何時配置設定到時間片真正開始執行,在java層面是不關心的,這些都是作業系統在背後暗箱操作,線程無論是正在執行還是等待執行,對于java來講都一并視為運作狀态。

當線程執行完畢,或者被stop()(廢棄方法,不建議使用)了,就會進入TERMINATED狀态。

上面講了個幸運的線程,從建立到結束,順順利利,沒有什麼波折,但是并非所有線程都能這麼幸運的。前面在講通用線程模型時,我們看到其實還有一種狀态叫休眠狀态,用來表示線程完全讓出cpu暫停執行的狀态,對于java線程來講,也有休眠狀态,隻不過将其細分成了三種狀态:

BLOCKED(阻塞), WAITING(等待), TIMED_WAITING(可逾時等待)

接下來我們具體分析一下這三種狀态。

當線程處于BLOCKED狀态,說明線程當下處于螢幕鎖(即synchronzied關鍵字修飾的鎖)的等待隊列中。

需要特别注意的是,此處的阻塞和調用阻塞api,等待傳回的阻塞是不一樣的,線程因為調用阻塞api而處于“阻塞狀态”,比如nio中的select()方法,此時線程隻是在作業系統層面處于休眠狀态,但是在jvm層面,仍将該線程視為運作。

最後需要強調一下,該狀态僅針對螢幕鎖,線程等待顯式鎖(基于AQS架構的鎖),并不會處于BLOCKED狀态,而是下文講的WAITING或TIMED_WAITING狀态。因為顯式鎖是基于LockSupport的park方法實作(後面會出一篇文章詳細講解LockSupport),而park方法會使線程進入WAITING或TIMED_WAITING狀态而非BLOCKED。

我們在java生命周期圖上加上BLOCKED狀态後,就成這樣了:

<img src="https://gitee.com/thomasChant/drawing-bed/raw/master/image-20210403201150287.png" alt="image-20210403201150287" style="zoom:100%;" />

waiting用于表示線程處于休眠等待狀态,有三種情況會使線程從RUNNABLE轉變為WAITING狀态

調用Thread.join();

調用LockSupport.park()/park(Object);

調用Object.wait();

對應的,滿足以下三種情況後,線程會從WAITING狀态,從新恢複運作變成RUNNABLE狀态:

等待的線程執行完畢,在Thread.join()等待的線程恢複運作;

調用LockSupport.unpark(Thread) 喚醒在Object.wait()方法等待的線程;

調用Object.notify()/notifyAll() 喚醒在LockSupport.park()/park(Object)方法等待的線程;

除此之外通過調用線程的interrupt()方法,也可以直接将處于WAITING的線程喚醒,轉為運作狀态。

以下是加上WAITING狀态後的狀态圖:

<img src="https://gitee.com/thomasChant/drawing-bed/raw/master/image-20210403201005664.png" alt="image-20210403201005664" style="zoom: 100%;" />

TIMED_WAITING狀态,也就是可逾時等待狀态,和等待狀态不同的地方在于,加了個逾時時間,也就是說明線程不會一直等,如果等待時間超過了設定的逾時時間,會自動回到運作狀态。

以下api會導緻線程進入可逾時等待狀态:

Thread.join(long)

Object.wait(long)

LockSupport.parkNanos(long)/parkUntil(long)

Thread.sleep(long)

相應的出現以下情況會使線程恢複到運作狀态:

等待的線程執行完畢,或者等待超出設定的時間,使阻塞在join(long)方法的線程恢複運作;

調用Object.notify()/notifyAll(),或者等待超出設定的時間,使阻塞在wait(long)方法的線程恢複運作;

調用LockSupport.unpark(Thread),或者等待超出設定的時間,使阻塞在LockSupport.parkNanos(long)/parkUntil(long)的線程恢複運作;

線程休眠時間超出設定的時間,使阻塞在Thread.sleep(long)的線程恢複運作

另外,處于TIMED_WAITING狀态線程,如果調用其interrupt()方法,可以使其恢複運作狀态。最終,我們的生命周期圖變成這樣了

<img src="https://gitee.com/thomasChant/drawing-bed/raw/master/image-20210403201108311.png" alt="image-20210403201108311" style="zoom:100%;" />

jstack工具

前面講過掌握了線程的生命周期,可以幫助我們更快速的定位和排查多線程相關的bug,具體應該怎麼做呢?介紹一種簡單有效的辦法,那就是jstack指令,這是jdk自帶的一個工具,可以列印java程序中所有的線程的狀态及堆棧,幫助我們快速定位問題。這裡結合一個簡單的案例,講解一下jstack的使用方式。代碼如下:

在這段代碼中,建立了兩個線程t1和t2,兩個線程啟動後,會有一個先拿到鎖,睡眠100s,處于TIMED_WAITING狀态,另一個則會阻塞直到先拿到鎖的線程釋放鎖才執行,處于BLOCKED狀态。我們使用jstack指令來驗證一下:

通過jps列出所有java程序,可以看到程序JstackTest的pid為72146

一文徹底搞懂java線程生命周期

執行jstack ,列印堆棧資訊。可以看到,結果是符合我們猜測的。

一文徹底搞懂java線程生命周期

總結

本文主要講解了通用線程生命周期和java線程生命周期,兩者的主要差别在于java線程生命周期在通用線程生命周期基礎上進行了簡化和擴充,簡化是指将可運作狀态和運作狀态合并為運作狀态,擴充是指将休眠狀态細分為阻塞狀态,等待狀态、逾時等待狀态,同時還詳細講解了java線程生命周期各種狀态之間是如何轉換的,最後介紹了如何通過jstack指令列印線程堆棧,檢視線程狀态。