3.Java 線程狀態之 BLOCKED


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,

這句話很長,可以拆成兩個簡單句來理解。

  1. A thread in the blocked state is waiting for a monitor lock to

    enter

    a synchronized block/method。

    一個處於 blocked 狀態的線程正在等待一個監視器鎖以進入一個同步的塊或方法。

  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 方法之后,以再次進入一個同步的塊或方法。

進入(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%,下同)

thread state blocked jvisualvm

圖上的“監視(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,這個過程是這樣的:

  1. 調用 wait 方法必須在同步塊中,即是要先獲取鎖並進入同步塊,這是第一次 enter。
  2. 而調用 wait 之后則會釋放該鎖,並進入此鎖的等待隊列(wait set)中。
  3. 當收到其它線程的 notify 或 notifyAll 通知之后,等待線程並不能立即恢復執行,因為停止的地方是在同步塊內,而鎖已經釋放了,所以它要重新獲取鎖才能再次進入(reenter)同步塊,然后從上次 wait 的地方恢復執行。這是第二次 enter,所以叫 reenter。
  4. 但鎖並不會優先給它,該線程還是要與其它線程去競爭鎖,這一過程跟 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);
}

簡要介紹一下以上代碼場景:

  1. 有一個賬戶對象,有存錢(deposit)和取錢(withdraw)方法,初始金額100元。
  2. 取錢線程先啟動,並進入(enter)同步塊,試圖取200元,發現錢不夠,調用 wait,鎖釋放,線程掛起(WAITING 狀態)。
  3. 10秒后存錢線程啟動,存入錢並通知(notify)取錢線程,但之后繼續在同步塊中睡眠,導致鎖沒有釋放。
  4. 取錢線程收到通知后,退出 WAITING 狀態,但已經不持有鎖,當試圖重新進入(reenter)同步塊以恢復執行時,因鎖尚未被存錢線程釋放,於是被阻塞(BLOCKED 狀態)。

監控的顯示:

thread state reenter blocked jvisualvm

如圖,取錢線程先是 WAITING,在收到通知因無法獲取鎖而阻塞(BLOCKED)。

總結

綜合來看這兩句話,兩層意思,其實還是一個意思,簡單地講,就是enter,reenter 也還是 enter,概括地講:

當因為獲取不到鎖而無法進入同步塊時,線程處於 BLOCKED 狀態。

如果有線程長時間處於 BLOCKED 狀態,要考慮是否發生了死鎖(deadlock)的狀況。

BLOCKED 狀態可以視作為一種特殊的 waiting,是傳統 waiting 狀態的一個細分:

image

由於還沒有講到 WAITING 狀態,而這里有涉及到了 wait 方法,所以上面對 wait 也稍微做了些分析,在下一章節,會更加詳細的分析 WAITING 和 TIMED_WAITING 這兩個狀態。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM