詳解Java線程狀態及狀態轉換

爲何要了解Java線程狀態

線程是 JVM 執行任務的最小單元,理解線程的狀態轉換是理解後續多線程問題的基礎。

Java線程狀態轉換圖

在這裏插入圖片描述
圖:線程間的相互轉換

Java線程有哪些狀態?

在 JVM 運行中,線程一共有 NEWRUNNABLEBLOCKEDWAITINGTIMED_WAITINGTERMINATED 六種狀態,這些狀態對應 Thread.State 枚舉類中的狀態。

Thread.State枚舉源碼:
爲方便閱讀,在此去掉了文檔註釋

public enum State {
	NEW,
	RUNNABLE,
	BLOCKED,
	WAITING,
	TIMED_WAITING,
	TERMINATED;
}

在給定的時間點,線程只能處於這些狀態中的一種狀態。這些狀態是不反映任何操作系統線程狀態的虛擬機狀態。

NEW,TERMINATED

這兩個狀態比較好理解,當創建一個線程後,還沒有調用start()方法時,線程處在 NEW 狀態,線程完成執行,退出後變爲TERMINATED終止狀態。

RUNNABLE

運行 Threadstart 方法後,線程進入 RUNNABLE 可運行狀態

/**
 * 程序目的:觀察線程的各種狀態
 * created at 2020-06-26 19:09
 * @author lerry
 */
class MyThread extends Thread {
	@Override
	public void run() {
		System.out.printf("%s線程運行\n", Thread.currentThread().getName());
	}
}

/**
 * 分別觀察創建線程後、start()後、和線程退出後的線程狀態。
 * 其中Thread.sleep(50);是爲了等待線程執行完
 */
public class ThreadStateDemo {
	public static void main(String[] args) throws InterruptedException {
		MyThread myThread = new MyThread();
		System.out.printf("創建線程後,線程的狀態爲:%s\n", myThread.getState());
		myThread.start();
		System.out.printf("調用start()方法後線程的狀態爲:%s\n", myThread.getState());
		// 休眠50毫秒,等待MyThread線程執行完
		Thread.sleep(50);
		System.out.printf("再次打印線程的狀態爲:%s\n", myThread.getState());

	}
}

輸出結果:

創建線程後,線程的狀態爲:NEW
調用start()方法後線程的狀態爲:RUNNABLE
Thread-0線程運行
再次打印線程的狀態爲:TERMINATED

我們可以看到,輸出結果符合預期。

  1. 在剛創建完線程後,狀態爲NEW
  2. 調用了start()方法後線程的狀態變爲:RUNNABLE
  3. 然後,我們看到了run()方法的執行,這個執行,是在主線程main打印了調用start()方法後線程的狀態爲:RUNNABLE輸出後執行的。
  4. 隨後,我們讓main線程休眠了50毫秒,等待MyThread線程退出
  5. 最後再打印MyThread線程的狀態,爲TERMINATED

BLOCKED

如圖左側所示,在運行態中的線程進入 synchronized 同步塊或者同步方法時,如果獲取鎖失敗,則會進入到 BLOCKED 狀態。當獲取到鎖後,會從 BLOCKED 狀態恢復到就緒狀態。

import lombok.extern.slf4j.Slf4j;

/**
 * 程序目的:觀察線程的BLOCKED狀態
 * created at 2020-06-26 19:09
 * @author lerry
 */
@Slf4j
public class ThreadBlockedStateDemo {

	public static void main(String[] args) {
		Thread threadA = new Thread(() -> method01(), "A-Thread");
		Thread threadB = new Thread(() -> method01(), "B-Thread");

		threadA.start();
		threadB.start();

		log.info("線程A的狀態爲:{}", threadA.getState());
		log.info("線程B的狀態爲:{}", threadB.getState());
	}

	/**
	 * 停頓10毫秒、模擬方法執行耗時
	 */
	public static synchronized void method01() {
		log.info("[{}]:開始執行主線程的方法", Thread.currentThread().getName());
		try {
			Thread.sleep(10);
		}
		catch (InterruptedException e) {
			e.printStackTrace();
		}
		log.info("[{}]:主線程的方法執行完畢", Thread.currentThread().getName());
	}
}

輸出結果:

2020-06-26 20:32:15.404 [A-Thread] INFO  com.hua.threadtest.state.ThreadBlockedStateDemo - [A-Thread]:開始執行主線程的方法
2020-06-26 20:32:15.404 [main    ] INFO  com.hua.threadtest.state.ThreadBlockedStateDemo - 線程A的狀態爲:RUNNABLE
2020-06-26 20:32:15.407 [main    ] INFO  com.hua.threadtest.state.ThreadBlockedStateDemo - 線程B的狀態爲:BLOCKED
2020-06-26 20:32:15.417 [A-Thread] INFO  com.hua.threadtest.state.ThreadBlockedStateDemo - [A-Thread]:主線程的方法執行完畢
2020-06-26 20:32:15.418 [B-Thread] INFO  com.hua.threadtest.state.ThreadBlockedStateDemo - [B-Thread]:開始執行主線程的方法
2020-06-26 20:32:15.430 [B-Thread] INFO  com.hua.threadtest.state.ThreadBlockedStateDemo - [B-Thread]:主線程的方法執行完畢

A線程優先獲得到了鎖,狀態爲RUNNABLE,這時,B線程處於BLOCKED狀態。
當A線程執行完畢後,B線程執行對應方法。

WAITING,TIMED_WAITING

如圖右側所示,運行中的線程還會進入等待狀態,這兩個等待一個是有超時時間的等待,例如調用 Object.waitThread.join 等;另外一個是無超時的等待,例如調用 Thread.join 或者 Locksupport.park等。這兩種等待都可以通過 notifyunpark 結束等待狀態並恢復到就緒狀態。

官方文檔說明爲:

A thread in the waiting state is waiting for another thread to perform a particular action. For example, a thread that has called Object.wait() on an object is waiting for another thread to call Object.notify() or Object.notifyAll() on that object. A thread that has called Thread.join() is waiting for a specified thread to terminate.

處於等待狀態的線程正在等待另一個線程執行特定的操作。
接下來我們來模擬一下線程的WAITING狀態:

import lombok.extern.slf4j.Slf4j;

/**
 * <pre>
 * 程序目的:觀察線程的WAITING狀態
 * 模擬:只有一個售票窗口的售票廳,有兩個粉絲都想買票。
 * 如果沒有票,他們就繼續等待、如果有票,則買票、然後離開售票廳。
 * 其中,工作人員會補票,補票之後,粉絲就可以買到票了。
 * </pre>
 * created at 2020-06-26 19:09
 * @author lerry
 */
@Slf4j
public class ThreadWaitingStateDemo {

	public static void main(String[] args) throws InterruptedException {
		Ticket ticket = new Ticket();
		Thread threadA = new Thread(() -> {
			synchronized (ticket) {

				while (ticket.getNum() == 0) {
					try {
						ticket.wait();
					}
					catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				ticket.buy();
			}
		}, "粉絲A");

		Thread threadB = new Thread(() -> {
			synchronized (ticket) {
				while (ticket.getNum() == 0) {
					try {
						ticket.wait();
					}
					catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				ticket.buy();
			}
		}, "粉絲B");

		threadA.start();
		threadB.start();

		// 確保A和B線程都運行起來
		Thread.sleep(10);
		log.info("粉絲A線程的狀態爲:{}", threadA.getState());
		log.info("粉絲B線程的狀態爲:{}", threadB.getState());

		Thread employeeThread = new Thread(() -> {
			synchronized (ticket) {
				if (ticket.getNum() == 0) {
					ticket.addTickt();
					ticket.notifyAll();
				}
			}
		}, "補票員");
		employeeThread.start();
	}

}

@Slf4j
class Ticket {

	/**
	 * 票的張數
	 */
	private int num = 0;

	public int getNum() {
		return num;
	}

	public void addTickt() {
		try {
			Thread.sleep(2_000);
		}
		catch (InterruptedException e) {
			e.printStackTrace();
		}
		log.info("補充票");
		this.num += 2;
	}

	/**
	 * 停頓10毫秒、模擬方法執行耗時
	 */
	public void buy() {
		log.info("[{}]:購買了一張票", Thread.currentThread().getName());
		log.info("[{}]:退出售票廳", Thread.currentThread().getName());
	}
}

輸出:

2020-06-26 21:26:37.938 [main    ] INFO  com.hua.threadtest.state.ThreadWaitingStateDemo - 粉絲A線程的狀態爲:WAITING
2020-06-26 21:26:37.945 [main    ] INFO  com.hua.threadtest.state.ThreadWaitingStateDemo - 粉絲B線程的狀態爲:WAITING
2020-06-26 21:26:39.948 [補票員     ] INFO  com.hua.threadtest.state.Ticket - 補充票
2020-06-26 21:26:39.949 [粉絲B     ] INFO  com.hua.threadtest.state.Ticket - [粉絲B]:購買了一張票
2020-06-26 21:26:39.949 [粉絲B     ] INFO  com.hua.threadtest.state.Ticket - [粉絲B]:退出售票廳
2020-06-26 21:26:39.949 [粉絲A     ] INFO  com.hua.threadtest.state.Ticket - [粉絲A]:購買了一張票
2020-06-26 21:26:39.949 [粉絲A     ] INFO  com.hua.threadtest.state.Ticket - [粉絲A]:退出售票廳

當修改ticket.wait();ticket.wait(10);後,輸出結果如下:

2020-06-26 21:27:10.704 [main    ] INFO  com.hua.threadtest.state.ThreadWaitingStateDemo - 粉絲A線程的狀態爲:TIMED_WAITING
2020-06-26 21:27:10.709 [main    ] INFO  com.hua.threadtest.state.ThreadWaitingStateDemo - 粉絲B線程的狀態爲:TIMED_WAITING
2020-06-26 21:27:12.714 [補票員     ] INFO  com.hua.threadtest.state.Ticket - 補充票
2020-06-26 21:27:12.714 [粉絲B     ] INFO  com.hua.threadtest.state.Ticket - [粉絲B]:購買了一張票
2020-06-26 21:27:12.714 [粉絲B     ] INFO  com.hua.threadtest.state.Ticket - [粉絲B]:退出售票廳
2020-06-26 21:27:12.715 [粉絲A     ] INFO  com.hua.threadtest.state.Ticket - [粉絲A]:購買了一張票
2020-06-26 21:27:12.715 [粉絲A     ] INFO  com.hua.threadtest.state.Ticket - [粉絲A]:退出售票廳

關於wait()放在while循環的疑問

爲什麼ticket.wait();要放在while (ticket.getNum() == 0)代碼塊中呢?既然這行代碼時讓線程等待着,那使用if不就行了?

我們設想一下,如果使用if,則在線程被喚醒後,會繼續往下執行,不再判斷條件是否符合,這時還是沒有票,粉絲也就購買不到票了。

我們看一下Object.wait()的官方doc說明:

As in the one argument version, interrupts and spurious wakeups are possible, and this method should always be used in a loop:
           synchronized (obj) {
               while (<condition does not hold>)
                   obj.wait();
               ... // Perform action appropriate to condition
           }

在一個參數版本中(wait方法),中斷和虛假的喚醒是可能的,這個方法應該總是在循環中使用。
我們再繼續看Object.wait(long timeout)的文檔說明:

A thread can also wake up without being notified, interrupted, or timing out, a so-called spurious wakeup. While this will rarely occur in practice, applications must guard against it by testing for the condition that should have caused the thread to be awakened, and continuing to wait if the condition is not satisfied. In other words, waits should always occur in loops
***
線程也可以在沒有通知、中斷或超時的情況下被喚醒,這就是所謂的假喚醒。雖然這種情況在實踐中很少發生,但應用程序必須通過測試導致線程被喚醒的條件來防止這種情況發生,如果條件不滿足,則繼續等待。換句話說,等待應該總是在循環中發生

所以,爲了避免很少發生假喚醒出現時程序發生不可預知的錯誤,建議把wait()調用放在循環語句中。這樣就算被假喚醒,也有條件語句的限制。
這也是爲何wait要放在循環語句中的一個原因。

BLOCKED 和 WAITING 狀態的區別和聯繫

表:處於等待狀態的各種細分狀態對比

BLOCKED WAITING TIMED_WAITING
何時會出現該狀態 進入synchronized 同步塊或者同步方法時,獲取鎖失敗時 Object.wait with no timeout
Thread.join with no timeout
LockSupport.park
Thread.sleep
Object.wait with timeout
Thread.join with timeout
LockSupport.parkNanos
LockSupport.parkUntil
重新正常執行的條件 競爭到鎖時 Object.notify()、Object.notifyAll() 等待時間結束、Object.notify()、Object.notifyAll()

簡單來說,處於BLOCKED狀態的線程,還是在競爭鎖的,一旦cpu有時間,它競爭到了鎖、就會執行。
但是WAITING狀態的線程則不去競爭鎖,需要等待被動通知、或者自己定的鬧鐘(等待時間)到了、再去競爭鎖。
一圖勝千言,在此引用一張國外一位大牛畫的圖:
在這裏插入圖片描述
圖:線程轉換-詳細.jpg

參考

Java 線程狀態之 WAITING - Just do 挨踢 - OSCHINA
其中提到的排隊上廁所的例子,讓人忍俊不禁,作者舉得例子太恰當了,寓教於樂,贊一個!
Java線程中wait狀態和block狀態的區別? - 知乎
Java 線程狀態之 TIMED_WAITING - Just do 挨踢 - OSCHINA

環境說明

  • java -version
java version "1.8.0_251"
Java(TM) SE Runtime Environment (build 1.8.0_251-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.251-b08, mixed mode)

  • OS:macOS High Sierra 10.13.4
  • 日誌:logback
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章