Java線程狀態
Java線程共有6個狀態,在java.lang.Thread.State中明確定義。
線程狀態圖:
1.NEW 尚未啓動的線程的狀態
2.Runnable 可運行的線程的狀態,包括等待CPU調度和正在運行的線程的狀態
3.Blocked 等待監視器鎖的阻塞線程的狀態,處於synchronized同步代碼塊或方法中被阻塞
4.Waiting 等待線程的狀態。通過下列不帶超時的方式:Object.wait、Thread.join、LockSupport.park
處於等待狀態的線程正在等待另一個線程執行特定的動作,如一個線程調用了Object.wait(),此時會等待另一個線程調用同一個對象的Object.notify()或Object.notifyAll()
5.Timed Waiting 指定等待時間的等待線程的狀態,通過下列帶超時的方式:
Thread.sleep、Object.wait、Thread.join、LockSupport.parkNanos、LockSupport.parkUntil
6.Terminated 終止線程的狀態。線程正常執行完成或出現異常而終止
線程間通信
要想實現多個線程之間的協同,如:線程執行先後順序、獲取某個線程執行的結果,終止某個線程等,涉及到線程之間的相互通訊,分爲下面四類:
- 文件共享
- 網絡共享
- 共享變量
- jdk提供的線程協調API
這裏主要講通過JDK提供的API實現線程間的協作,JDK中對於需要多線程協作完成某一任務的場景,提供了對應的API支持。多線程協作的經典場景是:生產者-消費者模型(線程等待、線程喚醒)。以生產者-消費者模型爲例,當消費者線程獲取消費對象時,如果獲取消費對象失敗,則消費者線程會進入等待狀態,此時需要生產者線程通知喚醒消費者線程,每當生產者線程生產消費對象時,會通知喚醒消費者線程。
1.suspend和resume
調用Thread.suspend可以掛起目標線程,通過Thread.resume可以恢復線程的執行。由於suspend掛起之後並不會釋放鎖,而且必須先suspend再resume才能喚醒,使用suspend容易出現死鎖代碼,因此suspend和resume已被棄用。
suspend死鎖的兩個示例
public static Object baozidian = null;
/** 死鎖的suspend/resume。 suspend並不會像wait一樣釋放鎖,故此容易寫出死鎖代碼 */
public void suspendResumeDeadLockTest1() throws Exception {
// 啓動線程
Thread consumerThread = new Thread(() -> {
if (baozidian == null) { // 如果沒包子,則進入等待
System.out.println("1、進入等待");
// 當前線程拿到鎖,然後掛起
synchronized (this) {
Thread.currentThread().suspend();
}
}
System.out.println("2、買到包子,回家");
});
consumerThread.start();
// 3秒之後,生產一個包子
Thread.sleep(3000L);
baozidian = new Object();
// 爭取到鎖以後,再恢復consumerThread
synchronized (this) {
consumerThread.resume();
}
System.out.println("3、通知消費者");
}
/** 由於先後順序導致程序永久掛起的suspend/resume */
public void suspendResumeDeadLockTest2() throws Exception {
// 啓動線程
Thread consumerThread = new Thread(() -> {
if (baozidian == null) {
System.out.println("1、沒包子,進入等待");
try { // 爲這個線程加上一點延時
Thread.sleep(5000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 這裏的掛起執行在resume後面
Thread.currentThread().suspend();
}
System.out.println("2、買到包子,回家");
});
consumerThread.start();
// 3秒之後,生產一個包子
Thread.sleep(3000L);
baozidian = new Object();
consumerThread.resume();
System.out.println("3、通知消費者");
consumerThread.join();
}
2.wait和notify
只能由同一對象鎖的持有者線程調用Object.wait,也就是在該對象的synchronized同步代碼塊裏,否則會拋出IllegalMonitorStateException。wait方法導致當前線程等待,加入到該對象的等待集合中,並且放棄當前持有的對象鎖。notify/notifyAll方法喚醒一個/所有該對象等待集合中的的線程。
雖然wait會自動解鎖,但是對順序有要求,如果在notify被調用之後,才調用wait方法,也會出現與suspend和resume一樣由於先後順序導致的線程永遠處於等待狀態。
3.park和unpark
park和unpark可以理解爲”許可“機制,線程調用LockSupport.park則會進入等待狀態,等待“許可”,unpark方法喚醒線程,爲特定線程提供“許可”。park和unpark方法不要求調用順序,如果先調用unpark,再調用park,線程會直接被”許可“繼續運行。但是unpark不會疊加,也就是說,連續多次調用unpark後第一次park會被“許可”直接運行,後續的park會進入等待。park/unpark雖然解決了順序問題,但是如果線程持有鎖,不會自動釋放鎖。
僞喚醒
僞喚醒是指線程並非因爲notify、notifyAll、unpark等方法調用而被喚醒,是由於更底層的其他原因導致的。
官方建議應該在循環中檢查等待條件,而不是簡單的用if判斷,原因是處於等待狀態的線程可能會收到錯誤報警和僞喚醒,如果不在循環中檢查等待條件,程序就可能會在沒有滿足條件的情況下繼續運行。
4.stop、destory和interrupt
用於線程終止
線程終止有多種方式:
- Thread.stop 強制終止線程,並且清除監視器鎖的信息,但是可能導致線程安全問題,JDK不建議用
- Thread.destory JDK爲實現該方法
- Thread.interrupt 現在常用的方法,如果目標線程處於Waiting或Timed Waiting等待狀態時,interrupt會生效,並且中斷狀態將被清除,並拋出InterruptedException。如果目標線程被I/O或NIO中的Channel阻塞,同樣,I/O操作會被中斷或者返回特殊異常值,達到終止線程的目的。如果以上條件都不滿足,則會設置此線程的中斷狀態
- 標誌位 代碼邏輯中如果是循環執行,可以增加一個共享變量標誌位,用於判斷程序是否繼續執行