在上一篇章中咱們談論了 WAITING 狀態,在這一篇章裏,咱們來看剩餘的最後的一個狀態:TIMED_WAITING(限時等待)。java
一個正在限時等待另外一個線程執行一個動做的線程處於這一狀態。緩存
A thread that is waiting for another thread to perform an action for up to a specified waiting time is in this state.多線程
更詳細的定義仍是看 javadoc(jdk8):app
帶指定的等待時間的等待線程所處的狀態。一個線程處於這一狀態是由於用一個指定的正的等待時間(爲參數)調用瞭如下方法中的其一:ide
對應的英文原文以下:oop
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:ui
Thread.sleep
Object.wait
with timeoutThread.join
with timeoutLockSupport.parkNanos
LockSupport.parkUntil
不難看出,TIMED_WAITING 與 WAITING 間的聯繫仍是很緊密的,主要差別在時限(timeout)參數上。this
另外則是 sleep 這一點上的不一樣。spa
實際上,在上一篇章中談到的沒有參數的 wait() 等價於 wait(0),而 wait(0) 它不是等0毫秒,偏偏相反,它的意思是永久的等下去,到天荒地老,除非收到通知。.net
具體可見 java 的源代碼及相應 javadoc,注意:同時又還存在一種特殊的狀況,所謂的「spurious wakeup」(虛假喚醒),咱們在下面再討論。
便是把本身再次活動的命運徹底交給了別人(通知者),那麼這樣會存在什麼問題呢?
在這裏,咱們仍是繼續上一篇章中的談到的車箱場景,如不清楚的參見 Java 線程狀態之 WAITING。
設想一種狀況,乘務員線程增長了廁紙,正當它準備執行 notify 時,這個線程因某種緣由被殺死了(持有的鎖也隨之釋放)。這種狀況下,條件已經知足了,但等待的線程卻沒有收到通知,還在傻乎乎地等待。
簡而言之,就是存在通知失效的狀況。這時,若是有個心機婊線程,她考慮得比較周全,她不是調用 wait(),而是調用 wait(1000),若是把進入 wait set 比喻成在裏面睡覺等待。那麼 wait(1000)至關於自帶設有倒計時 1000 毫秒的鬧鐘,換言之,她在同時等待兩個通知,並取決於哪一個先到:
這種狀況相似於雙保險。下面是一個動態的 gif 示意圖(空的電池表明條件不知足,粉色的乘務員線程負責增長紙張,帶有鬧鐘的乘客線程表明限時等待):
這樣,在通知失效的狀況下,她仍是有機會自我喚醒的,進而完成尿尿動做。
可見,一個線程,她帶不帶表(鬧鐘),差異仍是有的。其它死心眼的線程則等呀等,等到下面都溼了卻依舊可能等不來通知。用本山大叔的話來講:那憋得是至關難受。
如下代碼模擬了上述情形,此次,沒有讓乘務員線程執行通知動做,但限時等待的線程2仍是自我喚醒了:
@Test public 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(); // 一直等待的線程1 Thread 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毫秒的線程2 Thread 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_WAITING assertThat(passenger1.getState()).isEqualTo(Thread.State.WAITING); assertThat(passenger2.getState()).isEqualTo(Thread.State.TIMED_WAITING); // 此時的紙張數應爲0 assertThat(toilet.paperCount).isEqualTo(0); // 乘務員線程啓動 steward.start(); // 確保已經增長紙張 Thread.sleep(100); // 此時的紙張數應爲10 assertThat(toilet.paperCount).isEqualTo(10); // 確保線程2已經自我喚醒 Thread.sleep(1000); // 若是紙張已經被消耗一張,說明線程2已經成功自我喚醒 assertThat(toilet.paperCount).isEqualTo(9); }
雖然,前面說到沒有參數的 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 does not hold>) obj.wait(timeout); ... // Perform action appropriate to condition }
簡單講,要避免使用 if 的方式來判斷條件,不然一旦線程恢復,就繼續往下執行,不會再次檢測條件。因爲可能存在的「虛假喚醒」,並不意味着條件是知足的,這點甚至對簡單的「二人轉」的兩個線程的 wait/notify 狀況也須要注意。
另外,若是對於更多線程的狀況,好比「生產者和消費者」問題,一個生產者,兩個消費者,更加不能簡單用 if 判斷。由於可能用的是 notifyAll,兩個消費者同時起來,其中一個先搶到了鎖,進行了消費,等另外一個也搶到鎖時,可能條件又不知足了,因此仍是要繼續判斷,不能簡單認爲被喚醒了就是條件知足了。
關於此話題的更多信息,可參考:
進入 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 也交由讀者自行分析。
在說完了 BLOCKED,WAITING 和 TIMED_WAITING 後,咱們能夠綜合來看看它們,好比,阻塞與等待到底有什麼本質的區別呢?
顯然,BLOCKED 一樣能夠視做是一種特殊的,隱式的 wait/nofity 機制。等待的條件就是「有鎖仍是沒鎖」。
不過,這是一個不肯定的等待,可能等待(沒法獲取鎖時),也可能不等待(能獲取鎖)。陷入這種阻塞後也沒有自主退出的機制。
有一點須要注意的是,BLOCKED 狀態是與 Java 語言級別的 synchronized 機制相關的,咱們知道在 Java 5.0 以後引入了更多的機制(java.util.concurrent),除了能夠用 synchronized 這種內部鎖,也可使用外部的顯式鎖。
顯式鎖有一些更好的特性,如能中斷,能設置獲取鎖的超時,可以有多個條件等,儘管從表面上說,當顯式鎖沒法獲取時,咱們仍是說,線程被「阻塞」了,但卻未必是 BLOCKED 狀態。
當鎖可用時,其中的一個線程會被系統隱式通知,並被賦予鎖,從而得到在同步塊中的執行權。
顯然,等待鎖的線程與系統同步機制造成了一個協做關係。
對比來看, WAITING 狀態屬於主動地顯式地申請的阻塞,BLOCKED 則屬於被動的阻塞,但不管從字面意義仍是從根本上來看,並沒有本質的區別。
在前面咱們也已經說過,這三個狀態能夠認爲是傳統 waiting 狀態在 JVM 層面的一個細分。
最後,跟傳統進(線)程狀態劃分的一個最終對比:
關於 Java 線程狀態的全部分析就到此爲止。