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

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

人的生命週期

在上文中,咱們提到了線程通訊,在多線程系統中,不一樣的線程執行不一樣的任務;若是這些任務之間存在聯繫,那麼執行這些任務的線程之間就必須可以通訊,共同協調完成系統任務。linux

在本文中,咱們接着來講說線程通訊中的線程的生命週期。程序員

線程的生命週期

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

jdk 中的線程狀態

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

狀態之間的轉換以下圖所示:服務器

線程狀態運行流程

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

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

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

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

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

  • 就緒狀態: 線程對象調用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 多線程設計的魅力。

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

相關文章
相關標籤/搜索