Java 線程狀態之 BLOCKED
在上一篇章中,我們強調了 BLOCKED 狀態跟 I/O 的阻塞是不同的,它不是一般意義上的阻塞,而是特指被 synchronized 塊阻塞,即是跟線程同步有關的一個狀態。
BLOCKED 狀態的定義
前面已經說過 BLOCKED(阻塞) 的簡單定義為:
一個正在阻塞等待一個監視器鎖的線程處於這一狀態。(A thread that is blocked waiting for a monitor lock is in this state.)
更加詳細的定義可以參考 Thread.State 中的 javadoc:
/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
BLOCKED,
這句話很長,可以拆成兩個簡單句來理解。
-
A thread in the blocked state is waiting for a monitor lock to
enter
a synchronized block/method。
一個處於 blocked 狀態的線程正在等待一個監視器鎖以進入一個同步的塊或方法。
-
A thread in the blocked state is waiting for a monitor lock to
reenter
a synchronized block/method after calling Object.wait。
一個處於 blocked 狀態的線程正在等待一個監視器鎖,在其調用 Object.wait 方法之后,以再次進入一個同步的塊或方法。
進入(enter)同步塊時阻塞
先說第一句,這個比較好理解。
監視器鎖用於同步訪問,以達到多線程間的互斥。所以一旦一個線程獲取鎖進入同步塊,在其出來之前,如果其它線程想進入,就會因為獲取不到鎖而阻塞在同步塊之外,這時的狀態就是 BLOCKED。
注:這一狀態的進入及解除都不受我們控制,當鎖可用時,線程即從阻塞狀態中恢復。
我們可以用一些代碼來演示這一過程:
@Test
public void testBlocked() throws Exception {
class Counter {
int counter;
public synchronized void increase() {
counter++;
try {
Thread.sleep(30000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
Counter c = new Counter();
Thread t1 = new Thread(new Runnable() {
public void run() {
c.increase();
}
}, "t1線程");
t1.start();
Thread t2 = new Thread(new Runnable() {
public void run() {
c.increase();
}
}, "t2線程");
t2.start();
Thread.sleep(100); // 確保 t2 run已經得到執行
assertThat(t2.getState()).isEqualTo(Thread.State.BLOCKED);
}
以上定義了一個訪問計數器 counter,有一個同步的 increase 方法。t1 線程先進入,然后在同步塊里面睡覺,導致鎖遲遲無法釋放,t2 嘗試執行同步方法時就因無法獲取鎖而被阻塞了。
VisualVM 監控顯示了 t2 線程的狀態:(圖片看不清可將頁面放大至125%,下同)
圖上的“監視(monitor)”狀態即為 BLOCKED 狀態。可以看到在t1睡眠期間t2處於 BLOCKED 狀態。
BLOCKED 狀態可以視作是一種特殊的 WAITING,特指等待鎖。
wait 之后重進入(reenter)同步塊時阻塞
現在再次來看第二句:
\2. A thread in the blocked state is waiting for a monitor lock to reenter a synchronized block/method after calling Object.wait。
一個處於 blocked 狀態的線程正在等待一個監視器鎖,在其調用 Object.wait 方法之后,以再次進入一個同步的塊或方法。
這句話有點繞,也不好翻譯成一句簡潔的中文。如果沒有對 wait 的相關背景有較好的理解,則不容易理解這句話。我們在此把它稍微展開講一下。既然是 reenter,說明有兩次 enter,這個過程是這樣的:
- 調用 wait 方法必須在同步塊中,即是要先獲取鎖並進入同步塊,這是第一次 enter。
- 而調用 wait 之后則會釋放該鎖,並進入此鎖的等待隊列(wait set)中。
- 當收到其它線程的 notify 或 notifyAll 通知之后,等待線程並不能立即恢復執行,因為停止的地方是在同步塊內,而鎖已經釋放了,所以它要重新獲取鎖才能再次進入(reenter)同步塊,然后從上次 wait 的地方恢復執行。這是第二次 enter,所以叫 reenter。
- 但鎖並不會優先給它,該線程還是要與其它線程去競爭鎖,這一過程跟 enter 的過程其實是一樣的,因此也可能因為鎖已經被其它線程據有而導致 BLOCKED。
這一過程就是所謂的 reenter a synchronized block/method after calling Object.wait。
我們也用一段代碼來演示這一過程:
@Test
public void testReenterBlocked() throws Exception {
class Account {
int amount = 100; // 賬戶初始100元
public synchronized void deposit(int cash) { // 存錢
amount += cash;
notify();
try {
Thread.sleep(30000); // 通知后卻暫時不退出
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public synchronized void withdraw(int cash) { // 取錢
while (cash > amount) {
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
amount -= cash;
}
}
Account account = new Account();
Thread withdrawThread = new Thread(new Runnable() {
public void run() {
account.withdraw(200);
}
}, "取錢線程");
withdrawThread.start();
Thread.sleep(100); // 確保取錢線程已經得到執行
assertThat(withdrawThread.getState()).isEqualTo(Thread.State.WAITING);
Thread depositThread = new Thread(new Runnable() {
public void run() {
account.deposit(100);
}
}, "存錢線程");
Thread.sleep(10000); // 讓取錢線程等待一段時間
depositThread.start();
Thread.sleep(300); // 確保取錢線程已經被存錢線程所通知到
assertThat(withdrawThread.getState()).isEqualTo(Thread.State.BLOCKED);
}
簡要介紹一下以上代碼場景:
- 有一個賬戶對象,有存錢(deposit)和取錢(withdraw)方法,初始金額100元。
- 取錢線程先啟動,並進入(enter)同步塊,試圖取200元,發現錢不夠,調用 wait,鎖釋放,線程掛起(WAITING 狀態)。
- 10秒后存錢線程啟動,存入錢並通知(notify)取錢線程,但之后繼續在同步塊中睡眠,導致鎖沒有釋放。
- 取錢線程收到通知后,退出 WAITING 狀態,但已經不持有鎖,當試圖重新進入(reenter)同步塊以恢復執行時,因鎖尚未被存錢線程釋放,於是被阻塞(BLOCKED 狀態)。
監控的顯示:
如圖,取錢線程先是 WAITING,在收到通知因無法獲取鎖而阻塞(BLOCKED)。
總結
綜合來看這兩句話,兩層意思,其實還是一個意思,簡單地講,就是enter,reenter 也還是 enter,概括地講:
當因為獲取不到鎖而無法進入同步塊時,線程處於 BLOCKED 狀態。
如果有線程長時間處於 BLOCKED 狀態,要考慮是否發生了死鎖(deadlock)的狀況。
BLOCKED 狀態可以視作為一種特殊的 waiting,是傳統 waiting 狀態的一個細分:
由於還沒有講到 WAITING 狀態,而這里有涉及到了 wait 方法,所以上面對 wait 也稍微做了些分析,在下一章節,會更加詳細的分析 WAITING 和 TIMED_WAITING 這兩個狀態。