圖解線程面試專題,我給面試官上了一節線程課

來源:my.oschina.net/goldenshaw/blog/802620

圖解線程面試專題,我給面試官上了一節線程課

線程間的協作(cooperate)機制

面試官Q:你講下線程狀態中的WAITING狀態,什麼時候會處於這個狀態?什麼時候離開這個狀態?

小菜J 會心一笑…

一個正在無限期等待另一個線程執行一個特別的動作的線程處於WAITING狀態。

A thread that is waiting indefinitely for another thread to perform a particular action is in this state.

然而這裡並沒有詳細說明這個“特別的動作”到底是什麼,詳細定義還是看 javadoc(jdk8):

一個線程進入 WAITING 狀態是因為調用了以下方法:

不帶時限的 Object.wait 方法

不帶時限的 Thread.join 方法

LockSupport.park

然後會等其它線程執行一個特別的動作,比如:

一個調用了某個對象的 Object.wait 方法的線程會等待另一個線程調用此對象的 Object.notify() 或 Object.notifyAll()。

一個調用了 Thread.join 方法的線程會等待指定的線程結束。

對應的英文原文如下:

A thread is in the waiting state due to calling one of the following methods:

Object.wait with no timeout

Thread.join with no timeout

LockSupport.park

A thread in the waiting state is waiting for another thread to perform a particular action. For example, a thread that has called Object.wait() on an object is waiting for another thread to call Object.notify() or Object.notifyAll() on that object. A thread that has called Thread.join() is waiting for a specified thread to terminate.

線程間的協作(cooperate)機制

顯然,WAITING 狀態所涉及的不是一個線程的獨角戲,相反,它涉及多個線程,具體地講,這是多個線程間的一種協作機制。談到線程我們經常想到的是線程間的競爭(race),比如去爭奪鎖,但這並不是故事的全部,線程間也會有協作機制。

就好比在公司裡你和你的同事們,你們可能存在在晉升時的競爭,但更多時候你們更多是一起合作以完成某些任務。

wait/notify 就是線程間的一種協作機制,那麼首先,為什麼 wait?什麼時候 wait?它為什麼要等其它線程執行“特別的動作”?它到底解決了什麼問題?

wait 的場景

首先,為什麼要 wait 呢?簡單講,是因為條件(condition)不滿足。那麼什麼是條件呢?為方便理解,我們設想一個場景:

有一節列車車廂,有很多乘客,每個乘客相當於一個線程;裡面有個廁所,這是一個公共資源,且一次只允許一個線程進去訪問(畢竟沒人希望在上廁所期間還與他人共享~)。

競爭關係

假如有多個乘客想同時上廁所,那麼這裡首先存在的是競爭的關係。

如果將廁所視為一個對象,它有一把鎖,想上廁所的乘客線程需要先獲取到鎖,然後才能進入廁所。

Java 在語言級直接提供了同步的機制,也即是 synchronized 關鍵字:

synchronized(expression) {……}

它的機制是這樣的:對錶達式(expresssion)求值(值的類型須是引用類型(reference type)),獲取它所代表的對象,然後嘗試獲取這個對象的鎖:

如果能獲取鎖,則進入同步塊執行,執行完後退出同步塊,並歸還對象的鎖(異常退出也會歸還);

如果不能獲取鎖,則阻塞在這裡,直到能夠獲取鎖。

在一個線程還在廁所期間,其它同時想上廁所的線程被阻塞,處在該廁所對象的 entry set 中,處於 BLOCKED 狀態。

完事之後,退出廁所,歸還鎖。

之後,系統再在 entry set 中挑選一個線程,將鎖給到它。

對於以上過程,以下為一個 gif 動圖演示:

當然,這就是我們所熟悉的鎖的競爭過程。以下為演示的代碼:

@Test

public void testBlockedState() throws Exception {

class Toilet { // 廁所類

public void pee() { // 尿尿方法

try {

Thread.sleep(21000);// 研究表明,動物無論大小尿尿時間都在21秒左右

} catch (InterruptedException e) {

Thread.currentThread().interrupt();

}

}

}

Toilet toilet = new Toilet();

Thread passenger1 = new Thread(new Runnable() {

public void run() {

synchronized (toilet) {

toilet.pee();

}

}

});

Thread passenger2 = new Thread(new Runnable() {

public void run() {

synchronized (toilet) {

toilet.pee();

}

}

});

passenger1.start();

// 確保乘客1先啟動

Thread.sleep(100);

passenger2.start();

// 確保已經執行了 run 方法

Thread.sleep(100);

// 在乘客1在廁所期間,乘客2處於 BLOCKED 狀態

assertThat(passenger2.getState()).isEqualTo(Thread.State.BLOCKED);

}

條件

現在,假設有個女乘客,她搶到了鎖,進去之後褲子脫了一半,發現馬桶的墊圈紙沒了,於是拒絕尿。

或許是因為她比較講究衛生,怕直接坐上去會弄髒她白花花的屁股~

現在,條件出現了:有紙沒紙,這就是某種條件。

那麼,現在條件不滿足,這位女線程改怎麼辦呢?如果只是在裡面乾等,顯然是不行的。

這不就是人民群眾所深惡痛絕的“佔著茅坑不拉尿”嗎?

一方面,外面 entry set 中可能好多群眾還嗷嗷待尿呢(其中可能有很多大老爺線程,他們才不在乎有沒有馬桶墊圈紙~)

另一方面,假定外面同時有“乘務員線程”,準備進去增加墊圈紙,可你在裡面霸佔著不出來,別人也沒法進去,也就沒法加紙。

所以,當條件不滿足時,需要出來,要把鎖還回去,以使得諸如“乘務員線程”的能進去增加紙張。

#

等待是必要的嗎?

那麼出來之後是否一定需要等待呢?當然也未必。

這裡所謂“等待”,指的是使線程處於不再活動的狀態,即是從調度隊列中剔除。

如果不等待,只是簡單歸還鎖,用一個反覆的循環來判斷條件是否滿足,那麼還是可以再次回到調度隊列,然後期待在下一次被調度到的時候,可能條件已經發生變化:

比如某個“乘務員線程”已經在之前被調度並增加了裡面的墊圈紙。自然,也可能再次調度到的時候,條件依舊是不滿足的。

現在讓我們考慮一種比較極端的情況:廁所外一大堆的“女乘客線程”想進去方便,同時還有一個焦急的“乘務員線程”想進去增加廁紙。

如果線程都不等待,而廁所又是一個公共資源,無法併發訪問。調度器每次挑一個線程進去,挑中“乘務員線程”的幾率反而降低了,entry set 中很可能越聚越多無法完成方便的“女乘客線程”,“乘務員線程”被選中執行的幾率越發下降。

當然,同步機制會防止產生所謂的“飢餓(starvation)”現象,“乘務員線程”最終還是有機會執行的,只是系統運行的效率下降了。

所以,這會干擾正常工作的線程,擠佔了資源,反而影響了自身條件的滿足。另外,“乘務員線程”可能這段時間根本沒有啟動,此時,不願等待的“女乘客線程”不過是徒勞地進進出出,佔用了 CPU 資源卻沒有辦成正事。

效果上還是在這種沒有進展的進進出出中等待,這種情形類似於所謂的忙等待 (busy waiting)。

協作關係

綜上,等待還是有必要的,我們需要一種更高效的機制,也即是 wait/notify 的協作機制。

當條件不滿足時,應該調用 wait()方法,這時線程釋放鎖,並進入所謂的 wait set 中,具體的講,是進入這個廁所對象的 wait set 中:

這時,線程不再活動,不再參與調度,因此不會浪費 CPU 資源,也不會去競爭鎖了,這時的線程狀態即是 WAITING。

現在的問題是:她們什麼時候才能再次活動呢?顯然,最佳的時機是當條件滿足的時候。

之後,“乘務員線程”進去增加廁紙,當然,此時,它也不能只是簡單加完廁紙就完了,它還要執行一個特別的動作,也即是“通知(notify)”在這個對象上等待的女乘客線程:

大概就是向她們喊一聲:“有紙啦!趕緊去尿吧!”顯然,如果只是“女乘客線程”方面一廂情願地等待,她們將沒有機會再執行。

所謂“通知”,也即是把她們從 wait set 中釋放出來,重新進入到調度隊列(ready queue)中。

如果是 notify,則選取所通知對象的 wait set 中的一個線程釋放;

如果是 notifyAll,則釋放所通知對象的 wait set 上的全部線程。

整個過程如下圖所示:

對於上述過程,我們也給出以下 gif 動圖演示:

注意:哪怕只通知了一個等待的線程,被通知線程也不能立即恢復執行,因為她當初中斷的地方是在同步塊內,而此刻她已經不持有鎖,所以她需要再次嘗試去獲取鎖(很可能面臨其它線程的競爭),成功後才能在當初調用 wait 方法之後的地方恢復執行。(這也即是所謂的 “reenter after calling Object.wait”)

如果能獲取鎖,線程就從 WAITING 狀態變成 RUNNABLE 狀態;

否則,從 wait set 出來,又進入 entry set,線程就從 WAITING 狀態又變成 BLOCKED 狀態。

綜上,這是一個協作機制,“女乘客線程”和“乘務員線程”間存在一個協作關係。顯然,這種協作關係的存在,“女乘客線程”可以避免在條件不滿足時的盲目嘗試,也為“乘務員線程”的順利執行騰出了資源;同時,在條件滿足時,又能及時得到通知。協作關係的存在使得彼此都能受益。

生產者與消費者問題

不難發現,以上實質上也就是經典的“生產者與消費者”的問題:

乘務員線程生產廁紙,女乘客線程消費廁紙。當廁紙沒有時(條件不滿足),女乘客線程等待,乘務員線程添加廁紙(使條件滿足),並通知女乘客線程(解除她們的等待狀態)。接下來,女乘客線程能否進一步執行則取決於鎖的獲取情況。

代碼的演示:

在以下代碼中,演示了上述的 wait/notify 的過程:

@Test

public void testWaitingState() 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();

// 兩乘客線程

Thread[] passengers = new Thread[2];

for (int i = 0; i < passengers.length; i++) {

passengers[i] = 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();

}

}

});

}

// 乘務員線程

Thread steward = new Thread(new Runnable() {

public void run() {

synchronized (toilet) {

toilet.paperCount += 10;// 增加十張紙

toilet.notifyAll();// 通知所有在此對象上等待的線程

}

}

});

passengers[0].start();

passengers[1].start();

// 確保已經執行了 run 方法

Thread.sleep(100);

// 沒有紙,兩線程均進入等待狀態

assertThat(passengers[0].getState()).isEqualTo(Thread.State.WAITING);

assertThat(passengers[1].getState()).isEqualTo(Thread.State.WAITING);

// 乘務員線程啟動,救星來了

steward.start();

// 確保已經增加紙張並已通知

Thread.sleep(100);

// 其中之一會得到鎖,並執行 pee,但無法確定是哪個,所以用 "或 ||"

// 注:因為 pee 方法中實際調用是 sleep, 所以很快就從 RUNNABLE 轉入 TIMED_WAITING(sleep 時對應的狀態)

assertTrue(Thread.State.TIMED_WAITING.equals(passengers[0].getState())

|| Thread.State.TIMED_WAITING.equals(passengers[1].getState()));

// 其中之一則被阻塞,但無法確定是哪個,所以用 "或 ||"

assertTrue(

Thread.State.BLOCKED.equals(passengers[0].getState()) || Thread.State.BLOCKED.equals(passengers[1].getState()));

}

join場景及其它

從定義中可知,除了 wait/notify 外,調用 join 方法也會讓線程處於 WAITING 狀態。

join 的機制中並沒有顯式的 wait/notify 的調用,但可以視作是一種特殊的,隱式的 wait/notify 機制。

假如有 a,b 兩個線程,在 a 線程中執行 b.join(),相當於讓 a 去等待 b,此時 a 停止執行,等 b 執行完了,系統內部會隱式地通知 a,使 a 解除等待狀態,恢復執行。

換言之,a 等待的條件是 “b 執行完畢”,b 完成後,系統會自動通知 a。

關於 LockSupport.park 的情況則由讀者自行分析。

與傳統 waiting 狀態的關係

Thread.State.WAITING 狀態與傳統的 waiting 狀態類似:

結語

就以這段話自勉、共勉吧。越努力、越幸運,如果你不是官二代、富二代、紅二代,那麼請記住:勤奮才是改變你命運的唯一捷徑。

更多分享目前國內公司Java面試常問的問題,包含了15個模塊:Java基礎/語法、String相關、集合、多線程、IO流、網絡編程、異常處理、Web方面相關、設計模式、高級框架、微服務框架、數據庫、JVM、Linux操作、算法分析及手寫代碼。如下圖所示:

由於篇幅原因,在這答案就不做全部展示了,這些題我已經整理成pdf文檔免費分享給那些有需要的朋友

圖解線程面試專題,我給面試官上了一節線程課

同時整理也花費了蠻多時間,有需要的朋友可以幫忙轉發分享下然後私信關鍵詞【學習】即可獲取免費領取方式!

圖解線程面試專題,我給面試官上了一節線程課

歡迎在留言區留下你的觀點,一起討論提高。如果今天的文章讓你有新的啟發,學習能力的提升上有新的認識,歡迎轉發分享給更多人。


分享到:


相關文章: