1. 程式人生 > >執行緒的幾種狀態轉換

執行緒的幾種狀態轉換

執行緒在一定條件下,狀態會發生變化。執行緒一共有以下幾種狀態:

1、新建狀態(New):新建立了一個執行緒物件。

2就緒狀態(Runnable):執行緒物件建立後,其他執行緒呼叫了該物件的start()方法。該狀態的執行緒位於“可執行執行緒池”中,變得可執行,只等待獲取CPU的使用權即在就緒狀態的程序除CPU之外,其它的執行所需資源都已全部獲得。

3、執行狀態(Running)就緒狀態的執行緒獲取了CPU,執行程式程式碼。

4、阻塞狀態(Blocked)阻塞狀態是執行緒因為某種原因放棄CPU使用權,暫時停止執行。直到執行緒進入就緒狀態,才有機會轉到執行狀態。

阻塞的情況分三種:

(1)

等待阻塞:執行的執行緒執行wait()方法,該執行緒會釋放佔用的所有資源,JVM會把該執行緒放入“等待池”中。進入這個狀態後,是不能自動喚醒的,必須依靠其他執行緒呼叫notify()notifyAll()方法才能被喚醒,

(2)同步阻塞:執行的執行緒在獲取物件的同步鎖時,若該同步鎖被別的執行緒佔用,則JVM會把該執行緒放入“鎖池”中。

(3)其他阻塞:執行的執行緒執行sleep()join()方法,或者發出了I/O請求時,JVM會把該執行緒置為阻塞狀態。當sleep()狀態超時、join()等待執行緒終止或者超時、或者I/O處理完畢時,執行緒重新轉入就緒狀態。

5、死亡狀態(Dead)

執行緒執行完了或者因異常退出了run()方法,該執行緒結束生命週期。

執行緒變化的狀態轉換圖如下:


注:拿到物件的鎖標記,即為獲得了對該物件(臨界區)的使用許可權。即該執行緒獲得了執行所需的資源,進入“就緒狀態”,只需獲得CPU,就可以執行。因為當呼叫wait()後,執行緒會釋放掉它所佔有的“鎖標誌”,所以執行緒只有在此獲取資源才能進入就緒狀態。下面小小的作下解釋:
1
、執行緒的實現有兩種方式,一是繼承Thread類,二是實現Runnable介面,但不管怎樣,當我們new了這個物件後,執行緒就進入了初始狀態;
2
、當該物件呼叫了start()方法,就進入就緒狀態;
3
、進入就緒後,當該物件被作業系統選中,獲得

CPU時間片就會進入執行狀態;
4
、進入執行狀態後情況就比較複雜了
    4.1
run()方法或main()方法結束後,執行緒就進入終止狀態;
    4.2
、當執行緒呼叫了自身的sleep()方法或其他執行緒的join()方法,程序讓出CPU,然後就會進入阻塞狀態(該狀態既停止當前執行緒,但並不釋放所佔有的資源呼叫sleep ()函式後,執行緒不會釋放它的“鎖標誌”。)。當sleep()結束或join()結束後,該執行緒進入可執行狀態,繼續等待OS分配CPU時間片。典型地,sleep()被用在等待某個資源就緒的情形:測試發現條件不滿足後,讓執行緒阻塞一段時間後重新測試,直到條件滿足為止。
    4.3
、執行緒呼叫了yield()方法,意思是放棄當前獲得的CPU時間片,回到就緒狀態,這時與其他程序處於同等競爭狀態,OS有可能會接著又讓這個程序進入執行狀態;呼叫 yield() 的效果等價於排程程式認為該執行緒已執行了足夠的時間片從而需要轉到另一個執行緒。yield()只是使當前執行緒重新回到可執行狀態,所以執行yield()的執行緒有可能在進入到可執行狀態後馬上又被執行。
   4.4
、當執行緒剛進入可執行狀態(注意,還沒執行),發現將要呼叫的資源被synchroniza(同步),獲取不到鎖標記,將會立即進入鎖池狀態,等待獲取鎖標記(這時的鎖池裡也許已經有了其他執行緒在等待獲取鎖標記,這時它們處於佇列狀態,既先到先得),一旦執行緒獲得鎖標記後,就轉入就緒狀態,等待OS分配CPU時間片;

4.5. suspend() resume()方法:兩個方法配套使用,suspend()使得執行緒進入阻塞狀態,並且不會自動恢復,必須其對應的resume()被呼叫,才能使得執行緒重新進入可執行狀態。典型地,suspend() resume() 被用在等待另一個執行緒產生的結果的情形:測試發現結果還沒有產生後,讓執行緒阻塞,另一個執行緒產生了結果後,呼叫 resume()使其恢復。
   4.6
wait() notify() 方法:當執行緒呼叫wait()方法後會進入等待佇列(進入這個狀態會釋放所佔有的所有資源,與阻塞狀態不同),進入這個狀態後,是不能自動喚醒的,必須依靠其他執行緒呼叫notify()notifyAll()方法才能被喚醒(由於notify()只是喚醒一個執行緒,但我們由不能確定具體喚醒的是哪一個執行緒,也許我們需要喚醒的執行緒不能夠被喚醒,因此在實際使用時,一般都用notifyAll()方法,喚醒有所執行緒),執行緒被喚醒後會進入鎖池,等待獲取鎖標記。

wait() 使得執行緒進入阻塞狀態,它有兩種形式:

一種允許指定以毫秒為單位的一段時間作為引數;另一種沒有引數。前者當對應的 notify()被呼叫或者超出指定時間時執行緒重新進入可執行狀態即就緒狀態,後者則必須對應的 notify()被呼叫。當呼叫wait()後,執行緒會釋放掉它所佔有的“鎖標誌”從而使執行緒所在物件中的其它synchronized資料可被別的執行緒使用waite()notify()因為會對物件的“鎖標誌”進行操作,所以它們必須在synchronized函式或synchronizedblock中進行呼叫。如果在non-synchronized函式或non-synchronizedblock中進行呼叫,雖然能編譯通過,但在執行時會發生IllegalMonitorStateException的異常。

注意區別:初看起來wait()  notify() 方法suspend() resume() 方法對沒有什麼分別,但是事實上它們是截然不同的。區別的核心在於,前面敘述的suspend()及其它所有方法線上程阻塞時都不會釋放佔用的鎖(如果佔用了的話),而wait()  notify() 這一對方法則相反。

上述的核心區別導致了一系列的細節上的區別

首先,前面敘述的所有方法都隸屬於 Thread類,但是wait()  notify() 方法這一對卻直接隸屬於 Object 也就是說,所有物件都擁有這一對方法。初看起來這十分不可思議,但是實際上卻是很自然的,因為這一對方法阻塞時要釋放佔用的鎖,而鎖是任何物件都具有的,呼叫任意物件的 wait() 方法導致執行緒阻塞,並且該物件上的鎖被釋放。而呼叫任意物件的notify()方法則導致因呼叫該物件的 wait()方法而阻塞的執行緒中隨機選擇的一個解除阻塞(但要等到獲得鎖後才真正可執行)。

其次,前面敘述的所有方法都可在任何位置呼叫,但是wait()  notify() 方法這一對方法卻必須在 synchronized 方法或塊中呼叫,理由也很簡單,只有在synchronized方法或塊中當前執行緒才佔有鎖,才有鎖可以釋放。同樣的道理,呼叫這一對方法的物件上的鎖必須為當前執行緒所擁有,這樣才有鎖可以釋放。因此,這一對方法呼叫必須放置在這樣的 synchronized方法或塊中,該方法或塊的上鎖物件就是呼叫這一對方法的物件。若不滿足這一條件,則程式雖然仍能編譯,但在執行時會出現IllegalMonitorStateException異常。

wait() notify()方法的上述特性決定了它們經常和synchronized方法或塊一起使用,將它們和作業系統的程序間通訊機制作一個比較就會發現它們的相似性:synchronized方法或塊提供了類似於作業系統原語的功能,它們的執行不會受到多執行緒機制的干擾,而這一對方法則相當於 blockwake up 原語(這一對方法均宣告為 synchronized)。它們的結合使得我們可以實現作業系統上一系列精妙的程序間通訊的演算法(如訊號量演算法),並用於解決各種複雜的執行緒間通訊問題。

關於 wait() notify() 方法最後再說明兩點:

第一:呼叫notify() 方法導致解除阻塞的執行緒是從因呼叫該物件的 wait()方法而阻塞的執行緒中隨機選取的,我們無法預料哪一個執行緒將會被選擇,所以程式設計時要特別小心,避免因這種不確定性而產生問題。

第二:除了notify(),還有一個方法 notifyAll()也可起到類似作用,唯一的區別在於,呼叫 notifyAll()方法將把因呼叫該物件的 wait()方法而阻塞的所有執行緒一次性全部解除阻塞。當然,只有獲得鎖的那一個執行緒才能進入可執行狀態。

談到阻塞,就不能不談一談死鎖,略一分析就能發現,suspend()方法和不指定超時期限的wait()方法的呼叫都可能產生死鎖。遺憾的是,Java並不在語言級別上支援死鎖的避免,我們在程式設計中必須小心地避免死鎖。