Java線程狀態與線程通訊wait/notify,park/unpark機制

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  終止線程的狀態。線程正常執行完成或出現異常而終止

 

線程間通信

要想實現多個線程之間的協同,如:線程執行先後順序、獲取某個線程執行的結果,終止某個線程等,涉及到線程之間的相互通訊,分爲下面四類:

  1. 文件共享
  2. 網絡共享
  3. 共享變量
  4. 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操作會被中斷或者返回特殊異常值,達到終止線程的目的。如果以上條件都不滿足,則會設置此線程的中斷狀態
  • 標誌位  代碼邏輯中如果是循環執行,可以增加一個共享變量標誌位,用於判斷程序是否繼續執行
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章