每個事物都有其生命週期,也就是事物從出生開始到最終消亡這中間的整個過程;在其整個生命週期的歷程中,會有不同階段,每個階段對應着一種狀態,比如:人的一生會經歷從嬰幼兒、青少年、青壯年、中老年到最終死亡,離開這人世間,這是人一生的狀態;同樣的,線程作爲一種事物,也有生命週期,在其生命週期中也存在着不同的狀態,不同的狀態之間還會有互相轉換。
在上文中,我們提到了線程通信,在多線程系統中,不同的線程執行不同的任務;如果這些任務之間存在聯繫,那麼執行這些任務的線程之間就必須能夠通信,共同協調完成系統任務。
在本文中,我們接着來說說線程通信中的線程的生命週期。
線程的生命週期
我們先來查看jdk
文檔,在Java
中,線程有以下幾個狀態:
在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
):又可以細分成兩種狀態,ready
和running
,分別表示就緒狀態和運行狀態。
- 就緒狀態: 線程對象調用
start方法
之後,等待JVM
的調度(此時該線程並沒有運行),還未開始運行; - 運行狀態: 線程對象已獲得
JVM
調度,處在運行中;如果存在多個CPU
,那麼允許多個線程並行運行;
3.阻塞狀態(blocked
):處於運行中的線程因爲某些原因放棄CPU時間片
,暫時停止運行,就會進入阻塞狀態;此時JVM
不會給線程分配CPU時間片
,直到線程重新進入就緒狀態(ready
),纔有可能轉到運行狀態;
阻塞狀態只能先進入就緒狀態,進而由操作系統轉到運行狀態,不能直接進入運行狀態;阻塞狀態發生的兩種情況:
- 當
A線程
處於運行中,試圖獲取同步鎖時,但同步鎖卻被B線程
獲取,此時JVM
會把A線程
存到共享資源對象的鎖池中,A線程
進入阻塞狀態; - 當線程處於運行狀態,發出了
IO
請求時,該線程會進入阻塞狀態;
4.等待狀態(waiting
):運行中的線程調用了wait方法
(無參數的wait方法
),然後JVM
會把該線程儲存到共享資源的對象等待池中,該線程進入等待狀態;處於該狀態中的線程只能被其他線程喚醒;
5.計時等待狀態(timed waiting
):運行中的線程調用了帶參數的wait方法
或者sleep方法
,此狀態下的線程不會釋放同步鎖/同步監聽器,以下幾種情況都會進入計時等待狀態:
- 當處於運行中的線程,調用了
wait(long time)
方法,JVM
會把當前線程存在共享資源對象等待池中,線程進入計時等待狀態; - 當前線程執行了
sleep(long time)
方法,該線程進入計時等待狀態;
6.終止狀態(terminated
):也可以稱爲死亡狀態,表示線程終止,它的生命走到了盡頭;線程一旦終止,就不能再重啓啓動,否則會發生**IllegalThreadStateException
**;有以下幾種情況線程會進入終止狀態:
- 正常執行完
run方法
而退出,壽終正寢,屬於正常死亡; - 線程執行遇到異常而退出,線程中斷,屬於意外死亡;
線程控制
線程休眠:讓運行中的的線程暫停一段時間,進入計時等待狀態。
方法: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
調度,所以沒有執行。
後臺線程
後臺線程,在後臺運行的線程,其目的是爲其他線程提供服務,也稱爲“守護線程"。JVM
的垃圾回收線程就是典型的後臺線程。
在Java
中,開發者們通過代碼創建的線程默認都是前臺線程,如果想要轉爲後臺線程可以通過調用 setDaemon(true)
來實現,該方法必須在start方法
之前調用,否則會觸發 IllegalThreadStateException
異常,因爲線程一旦啓動,就無法對其做修改了。
由前臺線程創建的新線程除非特別設置,否則都是前臺線程,同理,後臺線程創建的新線程也是後臺線程。若是不知道某個線程是前臺線程還是後臺線程,可通過線程對象調用 isDaemon()
方法來判斷。
若所有的前臺線程都死亡,後臺線程自動死亡,若是前臺線程沒有結束,後臺線程是不會結束的。
線程優先級
每個線程都有優先級,優先級的高低只與線程獲得執行機會的次數多少有關,並非是線程優先級越高的就一定先執行,因爲哪個線程的先運行取決於CPU
的調度,無法通過代碼控制。
在Java
中,支持了從1 - 10
的10
個優先級,1
是最低優先級,10
是最高優先級,默認優先級是5
;jdk
文檔中的線程優先級如下圖所示:
MAX_PRIORITY=10
,最高優先級MIN_PRIORITY=1
,最低優先級NORM_PRIORITY=5
,默認優先級
Java
在Thread
類中提供了獲取、設置線程優先級的方法:
int getPriority()
:返回線程的優先級。
void setPriority(int newPriority)
: 設置線程的優先級。
每個線程在創建時都有默認優先級,主線程默認優先級爲5
,如果A線程創建了B線程
,那麼B線程
和A線程
具有相同優先級;雖然Java
中可設置的優先級有10
個,但不同的操作系統支持的線程優先級不同的,windows
支持的,linux
不見得支持;所以,一般情況下,不建議自定義,建議使用上述Thread
類中提供的三個優先級,因爲這三個優先級各個操作系統均支持。
線程禮讓
yield
方法:表示當前線程對象提示調度器自己願意讓出CPU時間片
,但是調度器卻不一定會採納,因爲調度器同樣也有選擇是否採納的自由,他可以選擇忽略該提示。
調用該方法之後,線程對象進入就緒狀態,所以完全有可能出現某個線程調用了yield()
之後,線程調度器又把它調度出來重新執行。
在開發中很少會使用到該方法,該方法主要用於調試或者測試,比如在多線程競爭條件下,讓錯誤重現現象或者更加明顯。
sleep方法
和yield方法
的區別:
- 共同點是都能使當前處於運行狀態的線程放棄
CPU時間片
,把運行的機會給其他線程; - 不同點在於:
sleep方法
會給其他線程運行機會,但是並不會在意其他線程的優先級;而yield方法
只會給相同優先級或者更高優先級的線程運行的機會; - 調用
sleep方法
後,線程進入計時等待狀態,而調用yield方法
後,線程進入就緒狀態;
線程組
在Java
中,ThreadGroup
類表示線程組,可以對屬於同組的線程進行集中管理,在創建線程對象時,可以通過構造器指定其所屬的線程組。
Thread(ThreadGroup group,String name);
如果A線程
創建了B線程
,如果沒有設置B線程
的分組,那麼B線程
會默認加入到A線程
的線程組;一旦線程加入某個線程組,該線程就會一直存在於該線程組中直至該線程死亡,也就是說一個線程只能有存在於一個線程組中,在一個線程的整個生命週期中,線程組一經設定,便不能中途修改。當Java
程序運行時,JVM
會創建名爲main
的線程組,在默認情況下,所有的線程都歸屬於該改線程組下。
線程組和定時器
在JDK的java.util包中提供了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類表示定時器執行的某一項任務;
通過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 多線程設計的魅力。
完結。老夫雖不正經,但老夫一身的才華!關注我,獲取更多編程科技知識。