理解java.lang.Thread.State:WAITING(停車)
一、概述
在本文中,我們將討論 Java 線程狀態——特別是Thread.State.WAITING
。我們將研究線程進入此狀態的方法以及它們之間的區別。最後,我們將仔細研究LockSupport
類,它提供了幾個用於同步的靜態實用程序方法。
2.進入Thread.State.WAITING
Java 提供了多種將線程置於WAITING
狀態的方法。
2.1。 Object.wait()
我們可以將線程置於WAITING
狀態的最標準方法之一是通過wait()
方法。當一個線程擁有一個對象的監視器時,我們可以暫停它的執行,直到另一個線程完成一些工作並使用notify()
方法將其喚醒。當執行暫停時,線程處於WAITING (on object monitor)
狀態,這也在程序的線程轉儲中報告:
"WAITING-THREAD" #11 prio=5 os_prio=0 tid=0x000000001d6ff800 nid=0x544 in Object.wait() [0x000000001de4f000]
java.lang.Thread.State: WAITING (on object monitor)
2.2. Thread.join()
我們可以用來暫停線程執行的另一種方法是通過join()
調用。當我們的主線程需要等待工作線程首先完成時,我們可以從主線程調用工作線程實例上的join()
。執行將暫停,主線程將進入WAITING
狀態,從jstack
報告為WAITING (on object monitor)
:
"MAIN-THREAD" #12 prio=5 os_prio=0 tid=0x000000001da4f000 nid=0x25f4 in Object.wait() [0x000000001e28e000]
java.lang.Thread.State: WAITING (on object monitor)
2.3. LockSupport.park()
最後,我們還可以使用LockSupport
類的park()
靜態方法將線程設置為WAITING
狀態。調用park()
將停止當前線程的執行並將其置於WAITING
狀態 - 更具體地說WAITING (parking)
jstack
報告將顯示 WAITING (parking) 狀態:
"PARKED-THREAD" #11 prio=5 os_prio=0 tid=0x000000001e226800 nid=0x43cc waiting on condition [0x000000001e95f000]
java.lang.Thread.State: WAITING (parking)
由於我們想更好地理解線程停放和取消停放,讓我們仔細看看它是如何工作的。
3. 停放線程
正如我們之前看到的,我們可以使用LockSupport
類提供的工具來停放和取消停放線程。這個類是Unsafe
類的一個包裝器,它的大部分方法都會立即委託給它。但是,由於Unsafe
被認為是內部 Java API 並且不應該使用, LockSupport
是我們可以訪問停車實用程序的官方方式。
3.1。如何使用LockSupport
使用LockSupport
很簡單。如果我們想停止線程的執行,我們調用park()
方法。我們不必提供對線程對象本身的引用——代碼會停止調用它的線程。
讓我們看一個簡單的停車示例:
public class Application {
public static void main(String[] args) {
Thread t = new Thread(() -> {
int acc = 0;
for (int i = 1; i <= 100; i++) {
acc += i;
}
System.out.println("Work finished");
LockSupport.park();
System.out.println(acc);
});
t.setName("PARK-THREAD");
t.start();
}
}
我們創建了一個最小的控制台應用程序,它累積從 1 到 100 的數字並將它們打印出來。如果我們運行它,我們會看到它打印的是Work finished
但不是結果。當然,這是因為我們之前調用了park()
。
要讓PARK-THREAD
完成,我們必須將其取消停放。為此,我們必須使用不同的線程。我們可以使用main
線程(運行main()
方法的線程)或創建一個新線程。
為簡單起見,讓我們使用main
線程:
t.setName("PARK-THREAD");
t.start();
Thread.sleep(1000);
LockSupport.unpark(t);
我們在main
線程中添加一秒休眠,讓PARK-THREAD
完成積累並自行停放。之後,我們通過調用unpark(Thread)
來取消它。正如預期的那樣,在 unparking 期間,我們必須提供對要啟動的線程對象的引用。
通過我們的更改,程序現在可以正確終止並打印結果5050
。
3.2.停放許可證
停車 API 的內部使用許可證。在實踐中,這就像單個許可證Semaphore
一樣工作。 park permit 用於內部管理線程的狀態, park()
方法使用它,而unpark()
使其可用。
由於每個線程只能有一個可用的許可,因此多次調用unpark()
方法沒有任何效果。單個park()
調用將禁用線程。
然而,有趣的是,停放的線程等待一個允許再次啟用它自己的許可。如果在調用park()
時許可已經可用,則線程永遠不會被禁用。許可被消耗, park()
調用立即返回,線程繼續執行。
我們可以通過刪除前面代碼片段中對sleep()
的調用來看到這種效果:
//Thread.sleep(1000);
LockSupport.unpark(t);
如果我們再次運行我們的程序,我們將看到PARK-THREAD
執行沒有延遲。這是因為我們立即調用unpark()
,這使得許可可用於park()
。
3.3.公園超載
LockSupport
類包含park(Object blocker)
重載方法。 blocker
參數是負責線程停放的同步對象。我們提供的對像不會影響那個停放過程,但是它會在線程轉儲中報告,這可以幫助我們診斷並發問題。
讓我們更改我們的代碼以包含一個同步器對象:
Object syncObj = new Object();
LockSupport.park(syncObj);
如果我們刪除對unpark()
的調用並再次運行應用程序,它將掛起。如果我們使用jstack
查看PARK-THREAD
在做什麼,我們將得到:
"PARK-THREAD" #11 prio=5 os_prio=0 tid=0x000000001e401000 nid=0xfb0 waiting on condition [0x000000001eb4f000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x000000076b4a8690> (a java.lang.Object)
正如我們所見,最後一行包含PARK-THREAD
正在等待的對象。這有助於調試目的,這就是為什麼我們應該更喜歡park(Object)
重載。
4. 停車與等待
由於這兩個 API 都為我們提供了相似的功能,我們應該更喜歡哪一個?通常, LockSupport
類及其工具被認為是低級 API。此外,API 很容易被誤用,導致難以理解的死鎖。大多數情況下,我們應該使用Thread
類的wait()
和join()
。
使用停車的好處是我們不需要進入synchronized
塊來禁用線程。這很重要,因為synchronized
塊在代碼中建立了先發生關係,這會強制刷新所有變量,如果不需要,可能會降低性能。然而,這種優化應該很少發揮作用。
5. 結論
在本文中,我們探索了LockSupport
類及其停車 API。我們研究瞭如何使用它來禁用線程,並在內部解釋了它是如何工作的。最後,我們將它與更常見的wait()/join()
API 進行了比較,並展示了它們的區別。
與往常一樣,可以在 GitHub 上找到代碼示例。