「JAVA」線程生命週期分階段詳解,哲學家們深感死鎖難解

每個事物都有其生命週期,也就是事物從出生開始最終消亡這中間的整個過程;在其整個生命週期的歷程中,會有不同階段,每個階段對應着一種狀態,比如:人的一生會經歷從嬰幼兒、青少年、青壯年、中老年到最終死亡,離開這人世間,這是人一生的狀態;同樣的,線程作爲一種事物,也有生命週期,在其生命週期中也存在着不同的狀態,不同的狀態之間還會有互相轉換。

人的生命週期

在上文中,我們提到了線程通信,在多線程系統中,不同的線程執行不同的任務;如果這些任務之間存在聯繫,那麼執行這些任務的線程之間就必須能夠通信,共同協調完成系統任務。

在本文中,我們接着來說說線程通信中的線程的生命週期。

線程的生命週期

我們先來查看jdk文檔,在Java 中,線程有以下幾個狀態:

jdk 中的線程狀態

Java 中,給定的時間點上,一個線程只能處於一種狀態,上述圖片中的這些狀態都是虛擬機狀態,並不是操作系統的線程狀態。線程對象的狀態存放在Thread類的內部類(State)中,是一個枚舉,存在着6種固定的狀態:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED

狀態之間的轉換如下圖所示:

線程狀態運行流程

下面就來對這些狀態一一解釋:

1.新建狀態(new): 使用new創建一個線程對象,僅僅在堆中分配內存空間,在調用start方法之前的線程所處的狀態;在此狀態下,線程還沒啓動,只是創建了一個線程對象存儲在堆中;比如:

   Thread t = new Thread(); //  此時t就屬於新建狀態

當新建狀態下的線程對象調用了start方法,該線程對象就從新建狀態進入可運行狀態(runnable);線程對象的start方法只能調用一次,多次調用會發生IllegalThreadStateException

2.可運行狀態(runnable):又可以細分成兩種狀態,readyrunning,分別表示就緒狀態和運行狀態

  • 就緒狀態: 線程對象調用start方法之後,等待JVM的調度(此時該線程並沒有運行),還未開始運行;
  • 運行狀態: 線程對象已獲得JVM調度,處在運行中;如果存在多個CPU,那麼允許多個線程並行運行;

就緒狀態和運行狀態

3.阻塞狀態(blocked):處於運行中的線程因爲某些原因放棄CPU時間片,暫時停止運行,就會進入阻塞狀態;此時JVM不會給線程分配CPU時間片,直到線程重新進入就緒狀態(ready,纔有可能轉到運行狀態;

阻塞狀態只能先進入就緒狀態,進而由操作系統轉到運行狀態,不能直接進入運行狀態阻塞狀態發生的兩種情況:

  1. A線程處於運行中,試圖獲取同步鎖時,但同步鎖卻被B線程獲取,此時JVM會把A線程存到共享資源對象的鎖池中,A線程進入阻塞狀態;
  2. 當線程處於運行狀態,發出了IO請求時,該線程會進入阻塞狀態;

4.等待狀態(waiting):運行中的線程調用了wait方法(無參數的wait方法),然後JVM會把該線程儲存到共享資源的對象等待池中,該線程進入等待狀態;處於該狀態中的線程只能被其他線程喚醒;

5.計時等待狀態(timed waiting):運行中的線程調用了帶參數的wait方法或者sleep方法,此狀態下的線程不會釋放同步鎖/同步監聽器,以下幾種情況都會進入計時等待狀態:

  1. 當處於運行中的線程,調用了wait(long time)方法,JVM會把當前線程存在共享資源對象等待池中,線程進入計時等待狀態;
  2. 當前線程執行了sleep(long time)方法,該線程進入計時等待狀態;

6.終止狀態(terminated):也可以稱爲死亡狀態,表示線程終止,它的生命走到了盡頭;線程一旦終止,就不能再重啓啓動,否則會發生**IllegalThreadStateException**;有以下幾種情況線程會進入終止狀態:

  1. 正常執行完run方法而退出,壽終正寢,屬於正常死亡;
  2. 線程執行遇到異常而退出,線程中斷,屬於意外死亡;

線程控制

線程休眠:讓運行中的的線程暫停一段時間,進入計時等待狀態

方法:static void sleep(long millis) 

調用sleep後,當前線程放棄CPU時間片,進入**計時等待狀態,**在指定時間段之內,調用sleep的線程不會獲得執行的機會,此狀態下的線程不會釋放同步鎖/同步監聽器

該方法更多的用於模擬網絡延遲,讓多線程併發訪問同一個資源的錯誤效果更明顯;也有讓程序的執行便於觀察的調用:

public static void main(String[] args) {				
    for (int i = 5; i > 0; i-- ) {        			
        System.out.println("還剩 " + i + " 秒");          		
        Thread.sleep(1000);        
    }  			
    System.out.println("時間到");  
}

聯合線程

線程的 join方法 表示一個線程等待另一個線程完成後才執行;join方法被調用之後,調用join方法的線程對象所在的線程處於阻塞狀態,調用join方法的線程對象進入運行狀態。之所以把這種方式稱爲聯合線程,是因爲通過join方法把當前線程和當前線程所在的線程聯合成一個線程。

public class JoinThreadDemo {
    public static void main(String []args) throws Exception {
       	System.out.println("開始");
		JoinThread join = new JoinThread();
		for (int i = 0; i < 50; i++) {
			System.out.println("i : " + i);
			if ( i == 10) {
				join.start();
			}
			if (i == 20) {
				join.join();
			}
		}
		System.out.println("結束");
    }
}

class JoinThread extends Thread {
	@Override
	public void run() {
		for (int i = 0; i < 50; i++) {
			System.out.println("join : " + i);
		}
	}
}

運行結果打印如下:

聯合線程運行結果

可以看到,當i = 20時,join線程對象開始執行,主線程(主函數)進入阻塞狀態,暫停執行;join線程對象運行完成後,主線程(主函數)才重新開始執行。那爲啥i=10時,join線程對象沒有執行呢?原因是雖然join線程對象調用了start方法,但還未獲得JVM調度,所以沒有執行。

join方法

後臺線程

後臺線程,在後臺運行的線程,其目的是爲其他線程提供服務,也稱爲“守護線程"。JVM的垃圾回收線程就是典型的後臺線程。

Java 中,開發者們通過代碼創建的線程默認都是前臺線程,如果想要轉爲後臺線程可以通過調用 setDaemon(true) 來實現,該方法必須在start方法之前調用,否則會觸發 IllegalThreadStateException 異常,因爲線程一旦啓動,就無法對其做修改了。

setDaenon 方法和 isDaemon 方法

由前臺線程創建的新線程除非特別設置,否則都是前臺線程,同理,後臺線程創建的新線程也是後臺線程。若是不知道某個線程是前臺線程還是後臺線程,可通過線程對象調用 isDaemon() 方法來判斷。

若所有的前臺線程都死亡,後臺線程自動死亡,若是前臺線程沒有結束,後臺線程是不會結束的。

後臺線程代碼示例

線程優先級

每個線程都有優先級,優先級的高低只與線程獲得執行機會的次數多少有關,並非是線程優先級越高的就一定先執行,因爲哪個線程的先運行取決於CPU的調度,無法通過代碼控制。

Java 中,支持了從1 - 1010個優先級,1是最低優先級,10是最高優先級,默認優先級是5jdk 文檔中的線程優先級如下圖所示:

線程優先級

  • MAX_PRIORITY=10,最高優先級
  • MIN_PRIORITY=1,最低優先級
  • NORM_PRIORITY=5,默認優先級

JavaThread類中提供了獲取、設置線程優先級的方法:

int getPriority() :返回線程的優先級

getPriority() :返回線程的優先級

void setPriority(int newPriority) : 設置線程的優先級
setPriority(int newPriority) : 設置線程的優先級

每個線程在創建時都有默認優先級,主線程默認優先級爲5,如果A線程創建了B線程,那麼B線程A線程具有相同優先級;雖然Java 中可設置的優先級有10個,但不同的操作系統支持的線程優先級不同的,windows支持的,linux不見得支持;所以,一般情況下,不建議自定義,建議使用上述Thread類中提供的三個優先級,因爲這三個優先級各個操作系統均支持。

線程禮讓

yield方法:表示當前線程對象提示調度器自己願意讓出CPU時間片,但是調度器卻不一定會採納,因爲調度器同樣也有選擇是否採納的自由,他可以選擇忽略該提示。

yield方法

調用該方法之後,線程對象進入就緒狀態,所以完全有可能出現某個線程調用了yield()之後,線程調度器又把它調度出來重新執行。

在開發中很少會使用到該方法,該方法主要用於調試或者測試,比如在多線程競爭條件下,讓錯誤重現現象或者更加明顯。

sleep方法yield方法的區別:

  1. 共同點是都能使當前處於運行狀態的線程放棄CPU時間片,把運行的機會給其他線程;
  2. 不同點在於:sleep方法會給其他線程運行機會,但是並不會在意其他線程的優先級;而yield方法只會給相同優先級或者更高優先級的線程運行的機會;
  3. 調用sleep方法
    後,線程進入計時等待狀態,而調用yield方法後,線程進入就緒狀態

線程組

Java 中,ThreadGroup類表示線程組,可以對屬於同組的線程進行集中管理,在創建線程對象時,可以通過構造器指定其所屬的線程組。

Thread(ThreadGroup group,String name);

如果A線程創建了B線程,如果沒有設置B線程的分組,那麼B線程會默認加入到A線程的線程組;一旦線程加入某個線程組,該線程就會一直存在於該線程組中直至該線程死亡,也就是說一個線程只能有存在於一個線程組中,在一個線程的整個生命週期中,線程組一經設定,便不能中途修改。當Java程序運行時,JVM會創建名爲main的線程組,在默認情況下,所有的線程都歸屬於該改線程組下。

線程組和定時器

在JDK的java.util包中提供了Timer類,使用此類可以定時執行特定的任務;

Timer 中的定時執行方法

其中有幾個常用的方法:

// 將指定的任務(task)安排在指定的時間(time)執行
void schedule(TimerTask task, Date time)

// 從指定的時間(firstTime)開始,按照某一週期(period),重複執行定時任務(task)
void	schedule(TimerTask task, Date firstTime, long period)

// 指定的任務在(task)一定的延時(delay)後執行
void	schedule(TimerTask task, long delay)

// 指定的任務(task),在一定的延時(delay)後,按一定週期(period)重複執行
void	schedule(TimerTask task, long delay, long period)

TimerTask類表示定時器執行的某一項任務;

TimerTask類

通過jdk 文檔中的描述,不難發現,TimerTask其實也是一個線程,具有線程的屬性和操作,所以通過前幾篇文章的介紹,這個類的使用已經很熟悉了。就不再這裏贅述了。

線程死鎖

線程死鎖,A線程在等待由B線程持有的鎖時,而B線程也在等待A線程持有的鎖,此時,這種線程現象稱爲線程死鎖;由於JVM不檢測也不試圖避免這種情況的發生,所以程序員必須要避免死鎖的發生。

多線程通信的時候很容易造成死鎖,線程死鎖無法解決,只能避免; 當多個線程都要訪問共享的資源A、B、C時,要保證每一個線程都按照相同的順序去訪問他們,比如都先訪問A,然後是B,最後纔是C

Java 的Thread類存在一些因死鎖被廢棄過時的方法

  • suspend(): 讓正在運行的線程放棄CPU時間片,暫停運行;
  • resume(): 讓暫停的線程恢復運行;

由上述兩個方法可能導致的的死鎖情況:

假設有A、B兩個線程,首先A線程獲得對象鎖,正在執行一個同步方法,如果B線程調用A線程suspend方法,此時A線程會暫停運行,並放棄CPU時間片,但是並不會釋放擁有的鎖,從而導致A、B兩個線程都處於等待中;B在等待A釋放鎖,而A已暫停,沒辦法釋放鎖;這樣就會出現無論A、B哪個線程都不能獲得鎖。

哲學家就餐問題

哲學家就餐的問題也是一個描述死鎖很好的例子,以下是問題描述(內容來源於百度百科):

假設有五位哲學家圍坐在一張圓形餐桌旁,做以下兩件事情之一:喫飯,或者思考。喫東西的時候,他們就停止思考,思考的時候也停止喫東西。餐桌中間有一大碗意大利麪(也可以是其他的食物,比如:米飯,因爲喫米飯必須用兩根筷子),每兩個哲學家之間有一隻餐叉。因爲用一隻餐叉很難喫到意大利麪,所以假設哲學家必須用兩隻餐叉喫東西,且他們只能使用自己左右手兩邊的那兩隻餐叉

哲學家從來不交談,這就很有可能產生死鎖,出現:**每個哲學家都拿着左手的餐叉,永遠都在等右邊的餐叉(或者相反),**永遠都喫不到東西,最後餓死。即使沒有死鎖,也很有可能耗盡服務器資源。

假設規定當哲學家等待另一隻餐叉超過五分鐘後就放下自己手裏的那一隻餐叉,並且再等五分鐘後進行下一次嘗試。這個策略消除了死鎖(系統總會進入到下一個狀態),但又會產生新的問題:如果五位哲學家在完全相同的時刻進入餐廳,並同時拿起左邊或者右邊的餐叉,那麼這些哲學家就會同時等待五分鐘,同時放下手中的餐叉,又再等五分鐘,哲學家任然會餓死

哲學家就餐問題

在實際的計算機問題中,缺乏餐叉可以類比爲缺乏共享資源。一種常用的計算機技術是資源加鎖,保證在某個時刻,資源只能被一個程序或一段代碼訪問;當一個程序想要使用的資源已經被另一個程序鎖定,它就等待資源解鎖。但是當多個程序涉及到加鎖的資源時,在某些情況下仍然可能發生死鎖。例如,某個程序需要訪問兩個文件,訪問了其中一個文件,另外一個文件被其他的線程鎖定,這兩個程序都在等待對方解鎖另一個文件,但這永遠不會發生。

所以在Java 多線程開發中,儘量避免死鎖問題,因爲發生這樣的問題真的很頭疼。儘量多熟悉,多實踐多線程中的理論和操作,從一次次的成功案例中體會Java 多線程設計的魅力。

完結。老夫雖不正經,但老夫一身的才華!關注我,獲取更多編程科技知識。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章