從哲學家就餐問題徹底認識死鎖


第一節  哲學家就餐問題

第二節  什么是死鎖

第三節  死鎖的定義

第四節  死鎖發生的條件

第五節  如何避免死鎖

    5.1 動態避免,銀行家算法(杠桿分配),在資源分配上下文章

    5.2 靜態避免,從任務代碼上避免死鎖

第六節  死鎖的綜合治理

第一節  哲學家就餐問題

假設有五位哲學家圍坐在一張圓形餐桌旁,做以下兩件事情之一:吃飯,或者思考。吃東西的時候,他們就停止思考,思考的時候也停止吃東西。餐桌中間有一大碗意大利面,每兩個哲學家之間有一雙筷子。因為用一只筷子很難吃到意大利面,所以假設哲學家必須用兩只筷子吃東西。他們只能使用自己左右手邊的那兩只筷子。如下圖所示:

 每個哲學家拿起左手右手邊的兩只筷子,五個哲學家五雙筷子,哲學家們愉快的吃起了面順帶對我表示了感謝,好了問題結束。(咦?好像哪里不對)

真正的問題是這樣的,每兩個哲學家之間只有一只筷子而不是一雙,問題如下圖所示:

哲學家必須用兩只筷子吃東西,而且他們只能使用自己左右手邊的那兩只筷子。

對於上面的問題,我們很自然的能想到一個算法:

1.等待左邊筷子可用,拿起左邊的筷子

2.等待右邊筷子可用,拿起右邊的筷子

3.吃飯

4.放下兩根筷子

顯然,在嘗試次數足夠的情況下,有一種情況是無法避免的:每個哲學家都拿起了左邊的筷子,並在等待右邊的筷子。因為右邊的筷子正在右側哲學家的手中,右側哲學家也在等待更右邊的哲學家放下筷子,大家互相持有他人等待的資源並等待着他人釋放,導致大家都得不到資源從而出於飢餓狀態,這就是死鎖。

第二節  什么是死鎖

我們知道,線程的執行需要資源,每個線程都會以某種順序使用資源,如果請求資源時被拒絕了,線程無法繼續向下執行,就會等待。線程使用資源總是有三步:

1.請求資源

2.使用資源

3.釋放資源

在操作系統中,線程在請求資源失敗時必須等待,這種等待有兩種方式。一是阻塞等待;而是非阻塞等待,也就是立即返回,做其它事情,然后再嘗試請求,或者失敗退出從而終止線程。

如果線程采用非阻塞的方式等待資源則不會發生死鎖,可以想象,一個哲學家請求右手邊的筷子失敗了,放下左手拿着的筷子放棄吃飯,繼續思考。那么左邊的哲學家便有了兩雙筷子,吃完放下兩雙筷子則左右的哲學家都可以得到兩雙筷子,整個資源分配的局面就活了,不會發生死鎖。

然而采用阻塞方式等待資源的話,死鎖便有可能發生,每個哲學家都拿着一直筷子等待另一只筷子可用,每個哲學家都不會放下手中的筷子,這樣大家都在無休止的等待。

如果有n個線程 T1~Tn ,以及n個資源 R1~Rn 。其中 Ti 持有資源 Ri ,但又請求資源 Ri+1 ,這樣便行成了死鎖。我們可以通過有向圖來表示這種持有和等待的關系,實線表示已經持有資源,虛線表示在等待該資源,如下圖所示:

 每個線程都在等待某一個資源,因此沒有線程可以推進,因此造成死鎖。

第三節  死鎖的定義

如果一組線程,每一個線程都在等待一個事件的發生(這里的事件通常指資源的釋放),而每個線程等待的事件都只能由線程組中另一個線程發出,則稱這組線程發生了死鎖。

我們來看一組明顯會造成死鎖的代碼:

//線程1
new
Thread(){ @Override public void run(){ synchronized(A){ synchronized(B){ doSomeThing.... } } } }
//線程2
new
Thread(){ @Override public void run(){ synchronized(B){ synchronized(A){ doSomeThing.... } } } }

如果 線程1 與 線程2 交替執行, 1 獲得鎖 A ;然后 2 執行 , 2 獲得鎖 B 。

1 請求 B 被拒絕,因為 B 被 2 持有; 2 請求 A 被拒絕 ,因為 A 被 1 持有。

至此,1 ,2 都不能執行,形成了如下循環等待:

 我們並不是說該程序一定會發生死鎖,如果線程 1 獲得兩個鎖后線程 2 才開始執行,就不會發生死鎖。但線程 1 與 2 交替運行時,發生死鎖的概率比不發生死鎖的概率還要大得多。我們不會討論有多大的概率發生死鎖,我們考慮死鎖問題的維度是 可能會發生死鎖 不可能發生死鎖

我們寫的程序當然最好是不可能發生死鎖的,這就要求我們明確定義出何時會發生死鎖並進行規避,那么死鎖發生的條件是什么呢?

第四節  死鎖發生的條件

死鎖的發生必須滿足四個條件:

1. 資源有限。 非常直觀的一個條件,正如第一節的問題,如果桌子上有五雙筷子。每個哲學家都可以同時獲得足夠的筷子去吃飯,死鎖便不會發生。

2. 持有等待。 即一個線程在請求新資源時,不會釋放已獲得的資源。如果哲學家請求右手邊的筷子失敗則放下左手邊的筷子,那么左邊的哲學家便會有兩只筷子,資源得以盤活,大家可以順序的拿起筷子吃飯。

3. 不能搶占。 如果一個哲學家請求右手邊的筷子失敗,就去搶奪右邊哲學家手里的筷子,那么他也會得到兩只筷子。

4.循環等待。 也就是第二節圖片中的例子,你等我我等你,大家的等待和持有關系形成了一個閉環。

以上四個條件對死鎖的形成來說缺一不可,我們只要打破其中一個條件,死鎖便不會發生。那么我們如何打破這些條件來避免死鎖呢?

第五節  如何避免死鎖

避免死鎖有兩種方式:動態避免靜態避免

5.1 動態避免,銀行家算法,在資源分配上下文章

首先說一個比較有趣的例子,銀行的杠桿,讓我們看看銀行是如何避免死鎖的。

補充:對於銀行來說,流動資金便是資源(計算機資源或信號量),貸款者便是資源請求方(線程)。

我們都知道,用戶進行貸款一定是有需要完成的目的,我們為了簡單起見,將用戶的目的統一假設為拿錢來做生意,而且用戶均遵守信用(有錢則會還款)。

如果用戶可以貸到其信用額度的最大值的話,可以達到用戶計划的預期,生意可以做成,用戶可以按時還款。

如果用戶得不到信用額度的最大值,將導致用戶成本不足,無法成功的完成生意,則沒有足夠的錢來償還貸款,造成銀行呆賬。

銀行的資金有限,那么銀行在放貸時必須考慮有沒有一種分配方式可以使其儲備滿足每個用戶的最大需求,即滿足每個用戶的最大信用額度的貸款。

比如假設銀行有12000的流動資金,此時有A、B、C三個人申請貸款。

A信用額度為4000元

B信用額度為6000元

C信用額度為10000元

而貸款要求為:

A申請2000元

B申請4000元

C申請3000元

如果銀行一次性全部同意並放款,銀行將只剩3000元流動資金,此時如果 C 繼續申請其剩下的7000元額度,銀行將無錢可放。C生意失敗導致銀行面臨 3000 元壞賬的風險。

而如果銀行只批准A與B的要求,則銀行將剩下6000元流動資金。無論 A 申請剩下的2000元額度還是 B 申請剩下的2000元額度,銀行都可以滿足。則A 、 B 都可以順利完成任務還款。而等待 A 與 B 均完成任務並還款后,再批准 C 的要求,這樣銀行剩下的流動資金為9000元,可以滿足 C 剩下的7000元的額度要求,順利支撐 C 完成生意。

也就是說,銀行在審核貸款要求時,必須考慮如果同意貸款,剩下的流動資金有沒有辦法使所有已經貸款的用戶都得到最大貸款額度的滿足。也就是說,每次貸款審核,銀行都要考慮發放該貸款是否會使銀行進入一種非安全狀態

這樣每個人的信用額度雖然都不會超過銀行的總流動資金,但是所有人的信用額度加起來將遠遠大於銀行的流動資金總數,這便是杠桿。而在杠桿下通過每筆貸款都經過仔細審核來動態的分配資源,便可以避免資金陷入貸款者的死鎖(互相等待他人還債后銀行放款給自己)。

07年美國次貸危機便是一個算法失誤導致死鎖的實例,很多公司與個人還不起貸款導致大批銀行的破產。

動態避免算法便是在每次進行資源分配時,都需要仔細計算,確保該資源的申請不會使系統進入一個不安全狀態。安全狀態是指我們能夠找到一種資源分配的方法和順序,使每個在運行的線程都可以得到其需要的資源。如果資源的分配將使系統進入不安全狀態,則拒絕。動態避免是一種在資源分配上下功夫的防止死鎖的手段。

事實上很少有程序采取動態避免的策略。第一個原因是算法的復雜,我們需要維護資源的使用情況以及線程的持有資源數及需要資源數等信息;第二個原因是我們很難提前預知每個線程到底需要多少資源;第三個原因是如果線程數較多,每次分配資源時的計算將占用大量的時間。

5.2  靜態避免,從任務代碼上避免死鎖

5.1小節我們討論了從資源分配上對死鎖進行動態避免的策略,下面我們討論一下如何從任務代碼上避免死鎖。

1. 消除非搶占條件。我們將資源變為可以搶占的,哲學家可以互相搶筷子。這個策略聽起來可行,但並不普適。比如如果搶占的資源不是筷子而是鎖,那么鎖將失去其原有的語義,這樣的后果是不堪設想的!

2. 消除持有等待。這個辦法是可行的,如果哲學家請求筷子失敗便放下手中的持有的筷子去睡覺,每次放下筷子都會叫醒旁邊睡覺的哲學家讓其再次嘗試(或者我們讓哲學家要么拿起兩只筷子,要么睡覺等待其他哲學家放下筷子時叫醒),哲學家們會順序的得到兩只筷子並去吃飯。

3. 上一小節我們討論了使用銀行家算法來進行動態的資源分配,用其解決哲學家問題是可行的,但算法將會比較復雜。我們換個角度從資源分配上來解決這個問題。之所以會發生死鎖,從資源的角度來說,如果五個哲學家同時拿起一只筷子,那么桌子上將沒有筷子,任何一個哲學家都無法進餐。我們如果只允許同一時間,最多有4個哲學家可以拿起筷子,那么四個哲學家中至少有一個可以拿到足夠的筷子進餐(桌子上還剩一支筷子),死鎖便不會發生。

這種解法其實也屬於銀行家算法,因為四個哲學家拿起筷子時,桌子上將只剩一只筷子。第五個哲學家嘗試拿筷子,我們判斷如果允許其拿起筷子,那么剩下的資源將無法滿足任何一個哲學家拿起兩只筷子筷子吃飯,隧拒絕其請求。

因為本問題中只有兩個角色,筷子與哲學家。即資源只有一種(筷子),所以依賴關系比較簡單,我們得以將銀行家算法靜態化。

4. 消除循環等待條件。如第三中的樣例,我們規定所有需要A和B的線程都需要先獲得A 再獲得B ,那么死鎖便不會發生。因為不會出現一個線程持有B 並等待A 的情況,便不會有循環等待發生。 對於哲學家就餐問題,我們也可以通過規定拿筷子的順序來打破循環等待。比如,我們可以給哲學家編號,奇數號碼的哲學家必須先拿左手邊的筷子,偶數號碼的哲學家必須先拿右手邊的筷子,如下圖所示:

 第六節 死鎖的綜合治理

前面幾個小節我們介紹了死鎖的定義、死鎖發生的條件、如何避免死鎖。

對於死鎖發生的四個必要條件:資源有限、資源不可搶占、持有等待、循環等待,我們只要打破其中任何一個,死鎖都不會發生。

事情看起來並不復雜,但事實卻是,經驗再豐富的程序員在進行多道編程時都很難完全避免死鎖。

主要原因是當資源與線程數增多時,很難確定其依賴關系。我們必須從所有可能的依賴關系中找出會發生死鎖的情況並進行治理,這在實際編程時是很難做到的(情況簡單除外)。

就比如哲學家就餐問題,其 PV 原語(同步、互斥關系)可能非常簡單:

P(leftChopsticks)
    P(rightChopsticks)
       eat();
    V(rightChopsticks)
V(leftChopsticks)

但是在各哲學家(線程)交替運行的情況下,其瞬時依賴關系會有非常多的可能。

我們除了通過考慮資源分配、順序的訪問資源以及非持有等待上進行設計盡量避免死鎖的發生外,在死鎖發生時也要第一時間保護現場並進行排查(復現在很多時候是非常困難的)。

我們可以通過 jps 命令查看需要查看的Java進程的vmid:

后通過 jstack 查看該進程中的堆棧情況:

向下拉,我們需要的信息在下面:

可以看到jvm發現了一個死鎖,Thread-0 與 Thread-1 間發生了循環等待。

也可以用 jconsole 命令來查看:

點擊“檢測死鎖”:

然后就可以查看發生死鎖的線程的狀態:


免責聲明!

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



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