面试官让我讲下线程的TIMED_WAITING状态,我又笑了
轉載自??面試官讓我講下線程的TIMED_WAITING狀態,我又笑了
面試官Q:你講下線程狀態中的WAITING狀態,什么時候會處于這個狀態?什么時候離開這個狀態?
小菜J 會心一笑,可以撮這里 ->?線程的WAITING狀態
面試官Q:那你繼續講下TIMED_WAITING狀態
小菜J,又笑了。
一個正在限時等待另一個線程執行一個動作的線程處于這一狀態。
A thread that is waiting for another thread to perform an action for up to a specified waiting time is in this state.
更詳細的定義還是看 javadoc:
帶指定的等待時間的等待線程所處的狀態。一個線程處于這一狀態是因為用一個指定的正的等待時間(為參數)調用了以下方法中的其一:
-
Thread.sleep
-
帶時限(timeout)的 Object.wait
-
帶時限(timeout)的 Thread.join
-
LockSupport.parkNanos
-
LockSupport.parkUntil
對應的英文原文如下:
Thread state for a waiting thread with a specified waiting time. A thread is in the timed waiting state due to calling one of the following methods with a specified positive waiting time:
-
Thread.sleep
-
Object.wait?with timeout
-
Thread.join?with timeout
-
LockSupport.parkNanos
-
LockSupport.parkUntil
不難看出,TIMED_WAITING 與 WAITING 間的聯系還是很緊密的,主要差異在時限(timeout)參數上。
另外則是 sleep 這一點上的不同。
timed_waiting 的場景
實際上,在上一篇章中談到的沒有參數的 wait() 等價于 wait(0),而 wait(0) 它不是等0毫秒,恰恰相反,它的意思是永久的等下去,到天荒地老,除非收到通知。
具體可見 java 的源代碼及相應 javadoc,注意:同時又還存在一種特殊的情況,所謂的“spurious wakeup”(虛假喚醒),我們在下面再討論。
即是把自己再次活動的命運完全交給了別人(通知者),那么這樣會存在什么問題呢?
在這里,我們還是繼續上一篇章中的談到的車廂場景,如不清楚的參見 Java 線程狀態之 WAITING。
設想一種情況,乘務員線程增加了廁紙,正當它準備執行 notify 時,這個線程因某種原因被殺死了(持有的鎖也隨之釋放)。這種情況下,條件已經滿足了,但等待的線程卻沒有收到通知,還在傻乎乎地等待。
簡而言之,就是存在通知失效的情況。這時,如果有個心機婊線程,她考慮得比較周全,她不是調用 wait(),而是調用 wait(1000),如果把進入 wait set 比喻成在里面睡覺等待。那么 wait(1000)相當于自帶設有倒計時 1000 毫秒的鬧鐘,換言之,她在同時等待兩個通知,并取決于哪個先到:
-
如果在1000毫秒內,她就收到了乘務員線程的通知從而喚醒,鬧鐘也隨之失效;
-
反之,超過1000毫秒,還沒收到通知,則鬧鐘響起,此時她則被鬧鐘喚醒。
這種情況類似于雙保險。下面是一個動態的 gif 示意圖(空的電池代表條件不滿足,粉色的乘務員線程負責增加紙張,帶有鬧鐘的乘客線程代表限時等待):
這樣,在通知失效的情況下,她還是有機會自我喚醒的,進而完成尿尿動作。
可見,一個線程,她帶不帶表(鬧鐘),差別還是有的。其它死心眼的線程則等呀等,等到下面都濕了卻依舊可能等不來通知。用本山大叔的話來說:那憋得是相當難受。
以下代碼模擬了上述情形,這次,沒有讓乘務員線程執行通知動作,但限時等待的線程2還是自我喚醒了:
@Testpublic void testTimedWaitingState() throws Exception {class Toilet { // 廁所類int paperCount = 0; // 紙張public void pee() { // 尿尿方法try {Thread.sleep(21000);// 研究表明,動物無論大小尿尿時間都在21秒左右} catch (InterruptedException e) {Thread.currentThread().interrupt();}}}Toilet toilet = new Toilet();// 一直等待的線程1Thread passenger1 = new Thread(new Runnable() {public void run() {synchronized (toilet) {while (toilet.paperCount < 1) {try {toilet.wait(); // 條件不滿足,等待} catch (InterruptedException e) {Thread.currentThread().interrupt();}}toilet.paperCount--; // 使用一張紙toilet.pee();}}});// 只等待1000毫秒的線程2Thread passenger2 = new Thread(new Runnable() {public void run() {synchronized (toilet) {while (toilet.paperCount < 1) {try {toilet.wait(1000); // 條件不滿足,但只等待1000毫秒} catch (InterruptedException e) {Thread.currentThread().interrupt();}}toilet.paperCount--; // 使用一張紙toilet.pee();}}});// 乘務員線程Thread steward = new Thread(new Runnable() {public void run() {synchronized (toilet) {toilet.paperCount += 10;// 增加十張紙// 粗心的乘務員線程,沒有通知到,(這里簡單把代碼注釋掉來模擬)// toilet.notifyAll();// 通知所有在此對象上等待的線程}}});passenger1.start();passenger2.start();// 確保已經執行了 run 方法Thread.sleep(100);// 沒有紙,兩線程均進入等待狀態,其中,線程2進入 TIMED_WAITINGassertThat(passenger1.getState()).isEqualTo(Thread.State.WAITING);assertThat(passenger2.getState()).isEqualTo(Thread.State.TIMED_WAITING);// 此時的紙張數應為0assertThat(toilet.paperCount).isEqualTo(0);// 乘務員線程啟動steward.start();// 確保已經增加紙張Thread.sleep(100);// 此時的紙張數應為10assertThat(toilet.paperCount).isEqualTo(10);// 確保線程2已經自我喚醒Thread.sleep(1000);// 如果紙張已經被消耗一張,說明線程2已經成功自我喚醒assertThat(toilet.paperCount).isEqualTo(9);}虛假喚醒(spurious wakeup)
雖然,前面說到沒有參數的 wait() 等價于 wait(0),意思是永久的等下去直到被通知到。但事實上存在所謂的 “spurious wakeup”,也即是“虛假喚醒”的情況,具體可見 Object.wait(long timeout) 中的 javadoc 說明:
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.
一個線程也能在沒有被通知、中斷或超時的情況下喚醒,也即所謂的“虛假喚醒”,雖然這點在實踐中很少發生,應用應該檢測導致線程喚醒的條件,并在條件不滿足的情況下繼續等待,以此來防止這一點。
換言之,wait 應該總是在循環中調用(waits should always occur in loops),javadoc 中給出了樣板代碼:
synchronized(obj){while(<condition doesnothold>) {obj.wait(timeout);...// Perform action appropriate to condition}}簡單講,要避免使用 if 的方式來判斷條件,否則一旦線程恢復,就繼續往下執行,不會再次檢測條件。由于可能存在的“虛假喚醒”,并不意味著條件是滿足的,這點甚至對簡單的“二人轉”的兩個線程的 wait/notify 情況也需要注意。
另外,如果對于更多線程的情況,比如“生產者和消費者”問題,一個生產者,兩個消費者,更加不能簡單用 if 判斷。因為可能用的是 notifyAll,兩個消費者同時起來,其中一個先搶到了鎖,進行了消費,等另一個也搶到鎖時,可能條件又不滿足了,所以還是要繼續判斷,不能簡單認為被喚醒了就是條件滿足了。
關于此話題的更多信息,可參考:
-
Doug Lea 的 《Concurrent Programming in Java (Second Edition)》3.2.3 節。
-
Joshua Bloch 的 《Effective Java Programming Language Guide》,“Prefer concurrency utilities to ?wait and ?notify”章節。
sleep 時的線程狀態
進入 TIMED_WAITING 狀態的另一種常見情形是調用的 sleep 方法,單獨的線程也可以調用,不一定非要有協作關系,當然,依舊可以將它視作為一種特殊的 wait/notify 情形。
這種情況下就是完全靠“自帶鬧鐘”來通知了。
另:sleep(0) 跟 wait(0) 是不一樣的,sleep 不存在無限等待的情況,sleep(0) 相當于幾乎不等待。
需要注意,sleep 方法沒有任何同步語義。通常,我們會說,sleep 方法不會釋放鎖。
javadoc中的確切說法是:The thread does not lose ownership of any monitors.(線程不會失去任何 monitor 的所有權)
而較為夸張的說法則是說 sleep 時會抱住鎖不放,這種說法不能說說錯了,但不是很恰當。
打個不太確切的比方,就好比你指著一個大老爺們說:“他下個月不會來大姨媽”,那么,我們能說你說錯了嗎?但是,顯得很怪異。
就鎖這個問題而言,確切的講法是 sleep 是跟鎖無關的。
JLS 中的說法是“It is important to note that neither? Thread.sleep nor? Thread.yield have any
synchronization semantics”。(sleep 和 yield 均無任何同步語義),另一個影響是,在它們調用的前后都無需關心寄存器緩存與內存數據的一致性(no flush or reload)
見《The Java Language Specification Java SE 7 Edition》17.3 Sleep and Yield
所以,如果線程調用 sleep 時是帶了鎖,sleep 期間則鎖還為線程鎖擁有。
比如在同步塊中調用 sleep(需要特別注意,或許你需要的是 wait 的方法!)
反之,如果線程調用 sleep 時沒有帶鎖(這也是可以的,這點與 wait 不同,不是非得要在同步塊中調用),那么自然也不會在sleep 期間“抱住鎖不放”。
壓根就沒有鎖,你讓它抱啥呢?而 sleep 君則完全是一臉懵逼:“鎖?啥是鎖?我沒聽過這玩意!”
帶 timeout 的 join 的情景與 wait(timeout) 原理類似,這里不再展開敘述。
LockSupport.parkNanos 和 parkUnitl 也交由讀者自行分析。
總結
最后,跟傳統進(線)程狀態劃分的一個最終對比:
總結
以上是生活随笔為你收集整理的面试官让我讲下线程的TIMED_WAITING状态,我又笑了的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 束缚带怎么正确使用 束缚带正确使用的方法
- 下一篇: 花卉的养殖方法 怎么养花比较好