文章很長,建議收藏起來,慢慢讀! 瘋狂創客圈為小伙伴奉上以下珍貴的學習資源:
- 瘋狂創客圈 經典圖書 : 《Netty Zookeeper Redis 高並發實戰》 面試必備 + 大廠必備 + 漲薪必備
- 瘋狂創客圈 經典圖書 : 《SpringCloud、Nginx高並發核心編程》 面試必備 + 大廠必備 + 漲薪必備
- 資源寶庫: Java程序員必備 網盤資源大集合 價值>1000元 隨便取 GO->【博客園總入口 】
- 獨孤九劍:Netty靈魂實驗 : 本地 100W連接 高並發實驗,瞬間提升Java內力
- 技術、面試交流:和大廠 小伙伴、技術高手、架構師 進行 純粹的的技術問題交流、探討求助、問題圍觀學習
推薦: 瘋狂創客圈 高質量 博文
高並發 必讀 的精彩博文 | |
---|---|
nacos 實戰(史上最全) | sentinel (史上最全+入門教程) |
Zookeeper 分布式鎖 (圖解+秒懂+史上最全) | Webflux(史上最全) |
SpringCloud gateway (史上最全) | TCP/IP(圖解+秒懂+史上最全) |
10分鍾看懂, Java NIO 底層原理 | Feign原理 (圖解) |
更多精彩博文 ..... | 請參見【 瘋狂創客圈 高並發 總目錄 】 |
史上最全 Java 面試題 28 專題 總目錄
死鎖(史上最全、定期更新)面試題
說明,此文的知識,非常基礎, 后續 架構師尼恩 將使用 視頻的方式,進行講解,視頻的內容具體請關注 瘋狂創客圈的視頻:《從菜鳥到大神 Java高並發核心編程》. 具體的文字或者內容升級,請掃架構師尼恩微信了解詳情
另外,此文的格式,由markdown 通過程序轉成而來,由於很多表格,沒有來的及調整, 更完善的版本,請參見瘋狂創客《Java面試紅寶書》最新版本。具體情況,可以掃架構師尼恩微信了解詳情
前言
首先介紹大廠的死鎖面試題,然后對死鎖做一個全面的解讀。
大廠的死鎖面試題
什么是死鎖?
所謂死鎖,是指多個進程在運行過程中因爭奪資源而造成的一種僵局,當進程處於這種僵持狀態時,若無外力作用,它們都將無法再向前推進。 因此我們舉個例子來描述,如果此時有一個線程A,按照先鎖a再獲得鎖b的的順序獲得鎖,而在此同時又有另外一個線程B,按照先鎖b再鎖a的順序獲得鎖。如下圖所示:
產生死鎖的原因?
可歸結為如下兩點:
a. 競爭資源
- 系統中的資源可以分為兩類:
- 可剝奪資源,是指某進程在獲得這類資源后,該資源可以再被其他進程或系統剝奪,CPU和主存均屬於可剝奪性資源;
- 另一類資源是不可剝奪資源,當系統把這類資源分配給某進程后,再不能強行收回,只能在進程用完后自行釋放,如磁帶機、打印機等。
- 產生死鎖中的競爭資源之一指的是競爭不可剝奪資源(例如:系統中只有一台打印機,可供進程P1使用,假定P1已占用了打印機,若P2繼續要求打印機打印將阻塞)
- 產生死鎖中的競爭資源另外一種資源指的是競爭臨時資源(臨時資源包括硬件中斷、信號、消息、緩沖區內的消息等),通常消息通信順序進行不當,則會產生死鎖
b. 進程間推進順序非法
- 若P1保持了資源R1,P2保持了資源R2,系統處於不安全狀態,因為這兩個進程再向前推進,便可能發生死鎖
- 例如,當P1運行到P1:Request(R2)時,將因R2已被P2占用而阻塞;當P2運行到P2:Request(R1)時,也將因R1已被P1占用而阻塞,於是發生進程死鎖
死鎖產生的4個必要條件?
產生死鎖的必要條件:
- 互斥條件:進程要求對所分配的資源進行排它性控制,即在一段時間內某資源僅為一進程所占用。
- 請求和保持條件:當進程因請求資源而阻塞時,對已獲得的資源保持不放。
- 不剝奪條件:進程已獲得的資源在未使用完之前,不能剝奪,只能在使用完時由自己釋放。
- 環路等待條件:在發生死鎖時,必然存在一個進程--資源的環形鏈。
解決死鎖的基本方法
一、預防死鎖:
- 資源一次性分配:一次性分配所有資源,這樣就不會再有請求了:(破壞請求條件)
- 只要有一個資源得不到分配,也不給這個進程分配其他的資源:(破壞請保持條件)
- 可剝奪資源:即當某進程獲得了部分資源,但得不到其它資源,則釋放已占有的資源(破壞不可剝奪條件)
- 資源有序分配法:系統給每類資源賦予一個編號,每一個進程按編號遞增的順序請求資源,釋放則相反(破壞環路等待條件)
1 以確定的順序獲得鎖
如果必須獲取多個鎖,那么在設計的時候需要充分考慮不同線程之前獲得鎖的順序。按照上面的例子,兩個線程獲得鎖的時序圖如下:
如果此時把獲得鎖的時序改成:
那么死鎖就永遠不會發生。 針對兩個特定的鎖,開發者可以嘗試按照鎖對象的hashCode值大小的順序,分別獲得兩個鎖,這樣鎖總是會以特定的順序獲得鎖,那么死鎖也不會發生。問題變得更加復雜一些,如果此時有多個線程,都在競爭不同的鎖,簡單按照鎖對象的hashCode進行排序(單純按照hashCode順序排序會出現“環路等待”),可能就無法滿足要求了,這個時候開發者可以使用銀行家算法,所有的鎖都按照特定的順序獲取,同樣可以防止死鎖的發生,該算法在這里就不再贅述了,有興趣的可以自行了解一下。
2 超時放棄
當使用synchronized關鍵詞提供的內置鎖時,只要線程沒有獲得鎖,那么就會永遠等待下去,然而Lock接口提供了boolean tryLock(long time, TimeUnit unit) throws InterruptedException方法,該方法可以按照固定時長等待鎖,因此線程可以在獲取鎖超時以后,主動釋放之前已經獲得的所有的鎖。通過這種方式,也可以很有效地避免死鎖。 還是按照之前的例子,時序圖如下:
二、避免死鎖:
- 預防死鎖的幾種策略,會嚴重地損害系統性能。因此在避免死鎖時,要施加較弱的限制,從而獲得 較滿意的系統性能。由於在避免死鎖的策略中,允許進程動態地申請資源。因而,系統在進行資源分配之前預先計算資源分配的安全性。若此次分配不會導致系統進入不安全的狀態,則將資源分配給進程;否則,進程等待。其中最具有代表性的避免死鎖算法是銀行家算法。
- 銀行家算法:首先需要定義狀態和安全狀態的概念。系統的狀態是當前給進程分配的資源情況。因此,狀態包含兩個向量Resource(系統中每種資源的總量)和Available(未分配給進程的每種資源的總量)及兩個矩陣Claim(表示進程對資源的需求)和Allocation(表示當前分配給進程的資源)。安全狀態是指至少有一個資源分配序列不會導致死鎖。當進程請求一組資源時,假設同意該請求,從而改變了系統的狀態,然后確定其結果是否還處於安全狀態。如果是,同意這個請求;如果不是,阻塞該進程知道同意該請求后系統狀態仍然是安全的。
三、檢測死鎖
-
首先為每個進程和每個資源指定一個唯一的號碼;
-
然后建立資源分配表和進程等待表。
死鎖檢測的工具
1、Jstack命令
jstack是java虛擬機自帶的一種堆棧跟蹤工具。jstack用於打印出給定的java進程ID或core file或遠程調試服務的Java堆棧信息。 Jstack工具可以用於生成java虛擬機當前時刻的線程快照。線程快照是當前java虛擬機內每一條線程正在執行的方法堆棧的集合,生成線程快照的主要目的是定位線程出現長時間停頓的原因,如線程間死鎖、死循環、請求外部資源導致的長時間等待等。 線程出現停頓的時候通過jstack來查看各個線程的調用堆棧,就可以知道沒有響應的線程到底在后台做什么事情,或者等待什么資源。
2、JConsole工具
Jconsole是JDK自帶的監控工具,在JDK/bin目錄下可以找到。它用於連接正在運行的本地或者遠程的JVM,對運行在Java應用程序的資源消耗和性能進行監控,並畫出大量的圖表,提供強大的可視化界面。而且本身占用的服務器內存很小,甚至可以說幾乎不消耗。
四、解除死鎖:
當發現有進程死鎖后,便應立即把它從死鎖狀態中解脫出來,常采用的方法有:
- 剝奪資源:從其它進程剝奪足夠數量的資源給死鎖進程,以解除死鎖狀態;
- 撤消進程:可以直接撤消死鎖進程或撤消代價最小的進程,直至有足夠的資源可用,死鎖狀態.消除為止;所謂代價是指優先級、運行代價、進程的重要性和價值等。
ok,介紹大廠的死鎖面試題之后,接下來,對死鎖做一個全面的解讀。
一、什么是死鎖
多線程以及多進程改善了系統資源的利用率並提高了系統 的處理能力。然而,並發執行也帶來了新的問題——死鎖。
死鎖是指兩個或兩個以上的進程(線程)在運行過程中因爭奪資源而造成的一種僵局(Deadly-Embrace) ) ,若無外力作用,這些進程(線程)都將無法向前推進。
下面我們通過一些實例來說明死鎖現象。
先看生活中的一個實例,2個人一起吃飯但是只有一雙筷子,2人輪流吃(同時擁有2只筷子才能吃)。某一個時候,一個拿了左筷子,一人拿了右筷子,2個人都同時占用一個資源,等待另一個資源,這個時候甲在等待乙吃完並釋放它占有的筷子,同理,乙也在等待甲吃完並釋放它占有的筷子,這樣就陷入了一個死循環,誰也無法繼續吃飯。。。
在計算機系統中也存在類似的情況。例如,某計算機系統中只有一台打印機和一台輸入設備,進程P1正占用輸入設備,同時又提出使用打印機的請求,但此時打印機正被進程P2 所占用,而P2在未釋放打印機之前,又提出請求使用正被P1占用着的輸入設備。這樣兩個進程相互無休止地等待下去,均無法繼續執行,此時兩個進程陷入死鎖狀態。
關於死鎖的一些結論:
- 參與死鎖的進程數至少為兩個
- 參與死鎖的所有進程均等待資源
- 參與死鎖的進程至少有兩個已經占有資源
- 死鎖進程是系統中當前進程集合的一個子集
- 死鎖會浪費大量系統資源,甚至導致系統崩潰。
舉一個例子:
如何解決上面的問題呢?正所謂知己知彼方能百戰不殆,我們要先了解什么情況會發生死鎖,才能知道如何避免死鎖,很幸運我們可以站在巨人的肩膀上看待問題
一個銀行轉賬經典案例:
賬戶 A 給賬戶 B 轉賬,賬戶 A 余額減少 100 元,賬戶 B 余額增加 100 元,這個操作要是原子性的
先來看程序:
class Account {
private int balance;
// 轉賬
synchronized void transfer(
Account target, int amt){
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
用 synchronized 直接保護 transfer 方法,然后操作 資源「Account 的A 余額」和資源「Account的B 余額」就可以了
其中有兩個問題:
- 單純的用 synchronized 方法起不到保護作用(不能保護 target)
- 用 Account.class 鎖方案,鎖的粒度又過大,導致涉及到賬戶的所有操作(取款,轉賬,修改密碼等)都會變成串行操作
如何解決這兩個問題呢?咱們先換好衣服穿越回到過去尋找一下錢庄,一起透過現象看本質,dengdeng deng.......
來到錢庄,告訴櫃員你要給鐵蛋兒轉 100 銅錢,這時櫃員轉身在牆上尋找你和鐵蛋兒的賬本,此時櫃員可能面臨三種情況:
- 理想狀態: 你和鐵蛋兒的賬本都是空閑狀態,一起拿回來,在你的賬本上減 100 銅錢,在鐵蛋兒賬本上加 100 銅錢,櫃員轉身將賬本掛回到牆上,完成你的業務
- 尷尬狀態: 你的賬本在,鐵蛋兒的賬本被其他櫃員拿出去給別人轉賬,你要等待其他櫃員把鐵蛋兒的賬本歸還
- 抓狂狀態: 你的賬本不在,鐵蛋兒的賬本也不在,你只能等待兩個賬本都歸還
放慢櫃員的取賬本操作,他一定是先拿到你的賬本,然后再去拿鐵蛋兒的賬本,兩個賬本都拿到(理想狀態)之后才能完成轉賬,用程序模型來描述一下這個拿取賬本的過程:
我們繼續用程序代碼描述一下上面這個模型:
class Account {
private int balance;
// 轉賬
void transfer(Account target, int amt){
// 鎖定轉出賬戶
synchronized(this) {
// 鎖定轉入賬戶
synchronized(target) {
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
}
}
這個解決方案看起來很完美,解決了文章開頭說的兩個問題,但真是這樣嗎?
我們剛剛說過的理想狀態是錢庄只有一個櫃員(既單線程)。隨着錢庄規模變大,牆上早已掛了非常多個賬本,錢庄為了應對繁忙的業務,開通了多個窗口,此時有多個櫃員(多線程)處理錢庄業務。
櫃員 1 正在辦理給鐵蛋兒轉賬的業務,但只拿到了你的賬本;櫃員 2 正在辦理鐵蛋兒給你轉賬的業務,但只拿到了鐵蛋兒的賬本,此時雙方出現了尷尬狀態,兩位櫃員都在等待對方歸還賬本為當前客戶辦理轉賬業務。
現實中櫃員會溝通,喊出一嗓子 老鐵,鐵蛋兒的賬本先給我用一下,用完還給你,但程序卻沒這么智能,synchronized 內置鎖非常執着,它會告訴你「死等」的道理,最終出現死鎖
如何解決死鎖呢?
Java 有了 synchronized 內置鎖,還發明了顯示鎖 Lock,是不是就為了治一治 synchronized 「死等」的執着呢??
如果你捉急,可以直接去閱讀第四節。
二、死鎖與飢餓
飢餓(Starvation)指一個進程一直得不到資源。
死鎖和飢餓都是由於進程競爭資源而引起的。飢餓一般不占有資源,死鎖進程一定占有資源。
三、資源的類型
3.1 可重用資源和消耗性資源
3.1.1 可重用資源(永久性資源)
可被多個進程多次使用,如所有硬件。
- 只能分配給一個進程使用,不允許多個進程共享。
- 進程在對可重用資源的使用時,須按照請求資源、使用資源、釋放資源這樣的順序。
- 系統中每一類可重用資源中的單元數目是相對固定的,進程在運行期間,既不能創建,也不能刪除。
3.1.2 消耗性資源(臨時性資源)
又稱臨時性資源,是由進程在運行期間動態的創建和消耗的。
- 消耗性資源在進程運行期間是可以不斷變化的,有時可能為0。
- 進程在運行過程中,可以不斷地創造可消耗性資源的單元,將它們放入該資源類的緩沖區中,以增加該資源類的單元數目。
- 進程在運行過程中,可以請求若干個可消耗性資源單元,用於進程自己消耗,不再將它們返回給該資源類中。
可消耗資源通常是由生產者進程創建,由消費者進程消耗。最典型的可消耗資源是用於進程間通信的消息。
3.2 可搶占資源和不可搶占資源
3.2.1 可搶占資源
可搶占資源指某進程在獲得這類資源后,該資源可以再被其他進程或系統搶占。對於這類資源是不會引起死鎖的。
CPU 和主存均屬於可搶占性資源。
3.2.2 不可搶占資源
一旦系統把某資源分配給該進程后,就不能將它強行收回,只能在進程用完后自行釋放。
磁帶機、打印機等屬於不可搶占性資源。
四、死鎖產生的原因
-
競爭不可搶占資源引起死鎖
通常系統中擁有的不可搶占資源,其數量不足以滿足多個進程運行的需要,使得進程在運行過程中,會因爭奪資源而陷入僵局,如磁帶機、打印機等。只有對不可搶占資源的競爭 才可能產生死鎖,對可搶占資源的競爭是不會引起死鎖的。 -
競爭可消耗資源引起死鎖
-
進程推進順序不當引起死鎖
進程在運行過程中,請求和釋放資源的順序不當,也同樣會導致死鎖。例如,並發進程 P1、P2分別保持了資源R1、R2,而進程P1申請資源R2,進程P2申請資源R1時,兩者都會因為所需資源被占用而阻塞。
信號量使用不當也會造成死鎖。進程間彼此相互等待對方發來的消息,結果也會使得這 些進程間無法繼續向前推進。例如,進程A等待進程B發的消息,進程B又在等待進程A 發的消息,可以看出進程A和B不是因為競爭同一資源,而是在等待對方的資源導致死鎖。
4.1 競爭不可搶占資源引起死鎖
如:共享文件時引起死鎖
系統中擁有兩個進程P1和P2,它們都准備寫兩個文件F1和F2。而這兩者都屬於可重用和不可搶占性資源。如果進程P1在打開F1的同時,P2進程打開F2文件,當P1想打開F2時由於F2已結被占用而阻塞,當P2想打開1時由於F1已結被占用而阻塞,此時就會無線等待下去,形成死鎖。
4.2 競爭可消耗資源引起死鎖
如:進程通信時引起死鎖
系統中擁有三個進程P1、P2和P3,m1、m2、m3是3可消耗資源。進程P1一方面產生消息m1,將其發送給P2,另一方面要從P3接收消息m3。而進程P2一方面產生消息m2,將其發送給P3,另一方面要從P1接收消息m1。類似的,進程P3一方面產生消息m3,將其發送給P1,另一方面要從P2接收消息m2。
如果三個進程都先發送自己產生的消息后接收別人發來的消息,則可以順利的運行下去不會產生死鎖,但要是三個進程都先接收別人的消息而不產生消息則會永遠等待下去,產生死鎖。
4.3 進程推進順序不當引起死鎖
上圖中,如果按曲線1的順序推進,兩個進程可順利完成;如果按曲線2的順序推進,兩個進程可順利完成;如果按曲線3的順序推進,兩個進程可順利完成;如果按曲線4的順序推進,兩個進程將進入不安全區D中,此時P1保持了資源R1,P2保持了資源R2,系統處於不安全狀態,如果繼續向前推進,則可能產生死鎖。
五、產生死鎖的四個必要條件
Coffman
總結出了四個條件說明可以發生死鎖的情形:
Coffman 條件
互斥條件:指進程對所分配到的資源進行排它性使用,即在一段時間內某資源只由一個進程占用。如果此時還有其它進程請求資源,則請求者只能等待,直至占有資源的進程用畢釋放。
不可剝奪條件:指進程已獲得的資源,在未使用完之前,不能被剝奪,只能在使用完時由自己釋放。
請求和保持條件:指進程已經保持至少一個資源,但又提出了新的資源請求,而該資源已被其它進程占有,此時請求進程阻塞,但又對自己已獲得的其它資源保持不放。
環路等待條件:指在發生死鎖時,必然存在一個進程——資源的環形鏈,即進程集合{P1,P2,···,Pn}中的 P1 正在等待一個 P2 占用的資源;P2 正在等待 P3 占用的資源,……,Pn 正在等待已被 P0 占用的資源。
這幾個條件很好理解,其中「互斥條件」是並發編程的根基,這個條件沒辦法改變。但其他三個條件都有改變的可能,也就是說破壞另外三個條件就不會出現上面說到的死鎖問題
5.1 互斥條件:
進程要求對所分配的資源(如打印機)進行排他性控制,即在一段時間內某資源僅為一個進程所占有。此時若有其他進程請求該資源,則請求進程只能等待。
5.2 不可剝奪條件:
進程所獲得的資源在未使用完畢之前,不能被其他進程強行奪走,即只能由獲得該資源的進程自己來釋放(只能是主動釋放)。
5.3 請求與保持條件:
進程已經保持了至少一個資源,但又提出了新的資源請求,而該資源已被其他進程占有,此時請求進程被阻塞,但對自己已獲得的資源保持不放。
5.4 循環等待條件:
存在一種進程資源的循環等待鏈,鏈中每一個進程已獲得的資源同時被 鏈中下一個進程所請求。即存在一個處於等待狀態的進程集合{Pl, P2, …, pn},其中Pi等 待的資源被P(i+1)占有(i=0, 1, …, n-1),Pn等待的資源被P0占有,如圖2-15所示。
直觀上看,循環等待條件似乎和死鎖的定義一樣,其實不然。按死鎖定義構成等待環所 要求的條件更嚴,它要求Pi等待的資源必須由P(i+1)來滿足,而循環等待條件則無此限制。 例如,系統中有兩台輸出設備,P0占有一台,PK占有另一台,且K不屬於集合{0, 1, …, n}。
Pn等待一台輸出設備,它可以從P0獲得,也可能從PK獲得。因此,雖然Pn、P0和其他 一些進程形成了循環等待圈,但PK不在圈內,若PK釋放了輸出設備,則可打破循環等待, 如圖2-16所示。因此循環等待只是死鎖的必要條件。
資源分配圖含圈而系統又不一定有死鎖的原因是同類資源數大於1。但若系統中每類資 源都只有一個資源,則資源分配圖含圈就變成了系統出現死鎖的充分必要條件。
以上這四個條件是死鎖的必要條件,只要系統發生死鎖,這些條件必然成立,而只要上述條件之一不滿足,就不會發生死鎖。
產生死鎖的一個例子:
/**
* 一個簡單的死鎖類
* 當DeadLock類的對象flag==1時(td1),先鎖定o1,睡眠500毫秒
* 而td1在睡眠的時候另一個flag==0的對象(td2)線程啟動,先鎖定o2,睡眠500毫秒
* td1睡眠結束后需要鎖定o2才能繼續執行,而此時o2已被td2鎖定;
* td2睡眠結束后需要鎖定o1才能繼續執行,而此時o1已被td1鎖定;
* td1、td2相互等待,都需要得到對方鎖定的資源才能繼續執行,從而死鎖。
*/
public class DeadLock implements Runnable {
public int flag = 1;
//靜態對象是類的所有對象共享的
private static Object o1 = new Object(), o2 = new Object();
@Override
public void run() {
System.out.println("flag=" + flag);
if (flag == 1) {
synchronized (o1) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println("1");
}
}
}
if (flag == 0) {
synchronized (o2) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println("0");
}
}
}
}
public static void main(String[] args) {
DeadLock td1 = new DeadLock();
DeadLock td2 = new DeadLock();
td1.flag = 1;
td2.flag = 0;
//td1,td2都處於可執行狀態,但JVM線程調度先執行哪個線程是不確定的。
//td2的run()可能在td1的run()之前運行
new Thread(td1).start();
new Thread(td2).start();
}
}
六、處理死鎖的方法
- 預防死鎖:通過設置某些限制條件,去破壞產生死鎖的四個必要條件中的一個或幾個條件,來防止死鎖的發生。
- 避免死鎖:在資源的動態分配過程中,用某種方法去防止系統進入不安全狀態,從而避免死鎖的發生。
- 檢測死鎖:允許系統在運行過程中發生死鎖,但可設置檢測機構及時檢測死鎖的發生,並采取適當措施加以清除。
- 解除死鎖:當檢測出死鎖后,便采取適當措施將進程從死鎖狀態中解脫出來。
七、 預防死鎖
通過設置某些限制條件,去破壞產生死鎖的四個必要條件中的一個或幾個條件,來防止死鎖的發生
破壞死鎖的條件:
-
破壞“互斥”條件:
就是在系統里取消互斥。若資源不被一個進程獨占使用,那么死鎖是肯定不會發生的。但一般來說在所列的四個條件中,“互斥”條件是無法破壞的。因此,在死鎖預防里主要是破壞其他幾個必要條件,而不去涉及破壞“互斥”條件。注意:互斥條件不能被破壞,否則會造成結果的不可再現性。
-
破壞“占有並等待”條件:
破壞“占有並等待”條件,就是在系統中不允許進程在已獲得某種資源的情況下,申請其他資源。即要想出一個辦法,阻止進程在持有資源的同時申請其他資源。
方法一:創建進程時,要求它申請所需的全部資源,系統或滿足其所有要求,或什么也不給它。這是所謂的 “ 一次性分配”方案。
方法二:要求每個進程提出新的資源申請前,釋放它所占有的資源。這樣,一個進程在需要資源S時,須先把它先前占有的資源R釋放掉,然后才能提出對S的申請,即使它可能很快又要用到資源R。 -
破壞“不可搶占”條件:
破壞“不可搶占”條件就是允許對資源實行搶奪。
方法一:如果占有某些資源的一個進程進行進一步資源請求被拒絕,則該進程必須釋放它最初占有的資源,如果有必要,可再次請求這些資源和另外的資源。
方法二:如果一個進程請求當前被另一個進程占有的一個資源,則操作系統可以搶占另一個進程,要求它釋放資源。只有在任意兩個進程的優先級都不相同的條件下,方法二才能預防死鎖。 -
破壞“循環等待”條件:
破壞“循環等待”條件的一種方法,是將系統中的所有資源統一編號,進程可在任何時刻提出資源申請,但所有申請必須按照資源的編號順序(升序)提出。這樣做就能保證系統不出現死鎖。
銀行轉賬經典案例中的死鎖的避免
解決前面的銀行轉賬經典案例的死鎖, 有以下方法
方法一:破壞請求和保持條件
每個櫃員都可以取放賬本,很容易出現互相等待的情況。要想破壞請求和保持條件,就要一次性拿到所有資源。
可以不允許櫃員都可以取放賬本,賬本要由單獨的賬本管理員來管理
也就是說賬本管理員拿取賬本是臨界區,如果只拿到其中之一的賬本,那么不會給櫃員,而是等待櫃員下一次詢問是否兩個賬本都在
//賬本管理員
public class AccountBookManager {
synchronized boolean getAllRequiredAccountBook( Object from, Object to){
if(拿到所有賬本){
return true;
} else{
return false;
}
}
// 歸還資源
synchronized void releaseObtainedAccountBook(Object from, Object to){
歸還獲取到的賬本
}
}
public class Account {
//單例的賬本管理員
private AccountBookManager accountBookManager;
public void transfer(Account target, int amt){
// 一次性申請轉出賬戶和轉入賬戶,直到成功
while(!accountBookManager.getAllRequiredAccountBook(this, target)){
return;
}
try{
// 鎖定轉出賬戶
synchronized(this){
// 鎖定轉入賬戶
synchronized(target){
if (this.balance > amt){
this.balance -= amt;
target.balance += amt;
}
}
}
} finally {
accountBookManager.releaseObtainedAccountBook(this, target);
}
}
}
方法二:破壞不可剝奪條件
上面已經給了你小小的提示,為了解決內置鎖的執着,Java 顯示鎖支持通知(notify/notifyall)和等待(wait),也就是說該功能可以實現喊一嗓子 老鐵,鐵蛋兒的賬本先給我用一下,用完還給你 的功能,
還有,可以通過 加鎖時限(線程嘗試獲取鎖的時候加上一定的時限,超過時限則放棄對該鎖的請求,並釋放自己占有的鎖)去解決。
下面是一個類似的例子。
在嘗試獲取鎖的時候加一個超時時間,這也就意味着在嘗試獲取鎖的過程中若超過了這個時限該線程則放棄對該鎖請求。若一個線程沒有在給定的時限內成功獲得所有需要的鎖,則會進行回退並釋放所有已經獲得的鎖,然后等待一段隨機的時間再重試。這段隨機的等待時間讓其它線程有機會嘗試獲取相同的這些鎖,並且讓該應用在沒有獲得鎖的時候可以繼續運行(譯者注:加鎖超時后可以先繼續運行干點其它事情,再回頭來重復之前加鎖的邏輯)。
以下是一個例子,展示了兩個線程以不同的順序嘗試獲取相同的兩個鎖,在發生超時后回退並重試的場景:
Thread 1 locks A
Thread 2 locks B
Thread 1 attempts to lock B but is blocked
Thread 2 attempts to lock A but is blocked
Thread 1’s lock attempt on B times out
Thread 1 backs up and releases A as well
Thread 1 waits randomly (e.g. 257 millis) before retrying.
Thread 2’s lock attempt on A times out
Thread 2 backs up and releases B as well
Thread 2 waits randomly (e.g. 43 millis) before retrying.
在上面的例子中,線程2比線程1早200毫秒進行重試加鎖,因此它可以先成功地獲取到兩個鎖。這時,線程1嘗試獲取鎖A並且處於等待狀態。當線程2結束時,線程1也可以順利的獲得這兩個鎖(除非線程2或者其它線程在線程1成功獲得兩個鎖之前又獲得其中的一些鎖)。
需要注意的是,由於存在鎖的超時,所以我們不能認為這種場景就一定是出現了死鎖。也可能是因為獲得了鎖的線程(導致其它線程超時)需要很長的時間去完成它的任務。
此外,如果有非常多的線程同一時間去競爭同一批資源,就算有超時和回退機制,還是可能會導致這些線程重復地嘗試但卻始終得不到鎖。如果只有兩個線程,並且重試的超時時間設定為0到500毫秒之間,這種現象可能不會發生,但是如果是10個或20個線程情況就不同了。因為這些線程等待相等的重試時間的概率就高的多(或者非常接近以至於會出現問題)。
(譯者注:超時和重試機制是為了避免在同一時間出現的競爭,但是當線程很多時,其中兩個或多個線程的超時時間一樣或者接近的可能性就會很大,因此就算出現競爭而導致超時后,由於超時時間一樣,它們又會同時開始重試,導致新一輪的競爭,帶來了新的問題。)
這種機制存在一個問題,在Java中不能對synchronized同步塊設置超時時間。需要使用 JUC 的顯式鎖。
方法三:破壞環路等待條件
破壞環路等待條件,就是按照相同的順序獲得鎖。
如果能確保所有的線程都是按照相同的順序獲得鎖,那么死鎖就不會發生。看下面這個例子:
Thread 1:
lock A
lock B
Thread 2:
wait for A
lock C (when A locked)
Thread 3:
wait for A
wait for B
wait for C
如果一個線程(比如線程3)需要一些鎖,那么它必須按照確定的順序獲取鎖。它只有獲得了從順序上排在前面的鎖之后,才能獲取后面的鎖。
例如,線程2和線程3只有在獲取了鎖A之后才能嘗試獲取鎖C(譯者注:獲取鎖A是獲取鎖C的必要條件)。因為線程1已經擁有了鎖A,所以線程2和3需要一直等到鎖A被釋放。然后在它們嘗試對B或C加鎖之前,必須成功地對A加了鎖。
按照順序加鎖是一種有效的死鎖預防機制。但是,這種方式需要你事先知道所有可能會用到的鎖(譯者注:並對這些鎖做適當的排序),但總有些時候是無法預知的。
破壞環路等待條件也很簡單,我們只需要將資源序號大小排序獲取就會解決這個問題,將環路拆除
按照id大小的順序來加鎖,先鎖住id 小的,然后才鎖住 id 大的
class Account {
private int id;
private int balance;
// 轉賬
void transfer(Account target, int amt){
Account smaller = this
Account larger = target;
// 排序
if (this.id > target.id) {
smaller = target;
larger = this;
}
// 鎖定序號小的賬戶
synchronized(smaller){
// 鎖定序號大的賬戶
synchronized(larger){
if (this.balance > amt){
this.balance -= amt;
target.balance += amt;
}
}
}
}
}
當 smaller 被占用時,其他線程就會被阻塞,也就不會存在死鎖了.
八 避免死鎖
理解了死鎖的原因,尤其是產生死鎖的四個必要條件,就可以最大可能地避免、預防和解除死鎖。所以,在系統設計、進程調度等方面注意如何讓這四個必要條件不成立,如何確定資源的合理分配算法,避免進程永久占據系統資源。此外,也要防止進程在處於等待狀態的情況下占用資源。因此,對資源的分配要給予合理的規划。
預防死鎖和避免死鎖的區別:
預防死鎖是設法至少破壞產生死鎖的四個必要條件之一,嚴格的防止死鎖的出現,而避免死鎖則不那么嚴格的限制產生死鎖的必要條件的存在,因為即使死鎖的必要條件存在,也不一定發生死鎖。避免死鎖是在系統運行過程中注意避免死鎖的最終發生。
常用避免死鎖的方法
有序資源分配法
這種算法資源按某種規則系統中的所有資源統一編號(例如打印機為1、磁帶機為2、磁盤為3、等等),申請時必須以上升的次序。系統要求申請進程:
1、對它所必須使用的而且屬於同一類的所有資源,必須一次申請完;
2、在申請不同類資源時,必須按各類設備的編號依次申請。例如:進程PA,使用資源的順序是R1,R2; 進程PB,使用資源的順序是R2,R1;若采用動態分配有可能形成環路條件,造成死鎖。
采用有序資源分配法:R1的編號為1,R2的編號為2;
PA:申請次序應是:R1,R2
PB:申請次序應是:R1,R2
這樣就破壞了環路條件,避免了死鎖的發生。
銀行家算法
詳見附錄一.
九 檢測死鎖
死鎖檢測是一個更好的死鎖預防機制,它主要是針對那些不可能實現按序加鎖並且鎖超時也不可行的場景。
每當一個線程獲得了鎖,會在線程和鎖相關的數據結構中(map、graph等等)將其記下。除此之外,每當有線程請求鎖,也需要記錄在這個數據結構中。
當一個線程請求鎖失敗時,這個線程可以遍歷鎖的關系圖看看是否有死鎖發生。例如,線程A請求鎖7,但是鎖7這個時候被線程B持有,這時線程A就可以檢查一下線程B是否已經請求了線程A當前所持有的鎖。如果線程B確實有這樣的請求,那么就是發生了死鎖(線程A擁有鎖1,請求鎖7;線程B擁有鎖7,請求鎖1)。
當然,死鎖一般要比兩個線程互相持有對方的鎖這種情況要復雜的多。線程A等待線程B,線程B等待線程C,線程C等待線程D,線程D又在等待線程A。線程A為了檢測死鎖,它需要遞進地檢測所有被B請求的鎖。從線程B所請求的鎖開始,線程A找到了線程C,然后又找到了線程D,發現線程D請求的鎖被線程A自己持有着。這是它就知道發生了死鎖。
下面是一幅關於四個線程(A,B,C和D)之間鎖占有和請求的關系圖。像這樣的數據結構就可以被用來檢測死鎖。
一般來說,由於操作系統有並發,共享以及隨機性等特點,通過預防和避免的手段達到排除死鎖的目的是很困難的。這需要較大的系統開銷,而且不能充分利用資源。為此,一種簡便的方法是系統為進程分配資源時,不采取任何限制性措施,但是提供了檢測和解脫死鎖的手段:能發現死鎖並從死鎖狀態中恢復出來。因此,在實際的操作系統中往往采用死鎖的檢測與恢復方法來排除死鎖。
死鎖檢測與恢復是指系統設有專門的機構,當死鎖發生時,該機構能夠檢測到死鎖發生的位置和原因,並能通過外力破壞死鎖發生的必要條件,從而使得並發進程從死鎖狀態中恢復出來。
這時進程P1占有資源R1而申請資源R2,進程P2占有資源R2而申請資源R1,按循環等待條件,進程和資源形成了環路,所以系統是死鎖狀態。進程P1,P2是參與死鎖的進程。
下面我們再來看一看死鎖檢測算法。算法使用的數據結構是如下這些:
占有矩陣A:nm階,其中n表示並發進程的個數,m表示系統的各類資源的個數,這個矩陣記錄了每一個進程當前占有各個資源類中資源的個數。
申請矩陣R:nm階,其中n表示並發進程的個數,m表示系統的各類資源的個數,這個矩陣記錄了每一個進程當前要完成工作需要申請的各個資源類中資源的個數。
空閑向量T:記錄當前m個資源類中空閑資源的個數。
完成向量F:布爾型向量值為真(true)或假(false),記錄當前n個並發進程能否進行完。為真即能進行完,為假則不能進行完。
臨時向量W:開始時W:=T。
算法步驟:
(1)W:=T,
對於所有的i=1,2,…,n,
如果A[i]=0,則F[i]:=true;否則,F[i]:=false
(2)找滿足下面條件的下標i:
F[i]:=false並且R[i]〈=W
如果不存在滿足上面的條件i,則轉到步驟(4)。
(3)W:=W+A[i]
F[i]:=true
轉到步驟(2)
(4)如果存在i,F[i]:=false,則系統處於死鎖狀態,且Pi進程參與了死鎖。什么時候進行死鎖的檢測取決於死鎖發生的頻率。如果死鎖發生的頻率高,那么死鎖檢測的頻率也要相應提高,這樣一方面可以提高系統資源的利用率,一方面可以避免更多的進程卷入死鎖。如果進程申請資源不能滿足就立刻進行檢測,那么每當死鎖形成時即能被發現,這和死鎖避免的算法相近,只是系統的開銷較大。為了減小死鎖檢測帶來的系統開銷,一般采取每隔一段時間進行一次死鎖檢測,或者在CPU的利用率降低到某一數值時,進行死鎖的檢測。
十 解除死鎖
一旦檢測出死鎖,就應立即釆取相應的措施,以解除死鎖。
死鎖解除的主要方法有:
-
資源剝奪法。掛起某些死鎖進程,並搶占它的資源,將這些資源分配給其他的死鎖進程。但應防止被掛起的進程長時間得不到資源,而處於資源匱乏的狀態。
-
撤銷進程法。強制撤銷部分、甚至全部死鎖進程並剝奪這些進程的資源。撤銷的原則可以按進程優先級和撤銷進程代價的高低進行。
-
進程回退法。讓一(多)個進程回退到足以回避死鎖的地步,進程回退時自願釋放資源而不是被剝奪。要求系統保持進程的歷史信息,設置還原點。
附錄1:銀行家算法
背景簡介
在銀行中,客戶申請貸款的數量是有限的,每個客戶在第一次申請貸款時要聲明完成該項目所需的最大資金量,在滿足所有貸款要求時,客戶應及時歸還。銀行家在客戶申請的貸款數量不超過自己擁有的最大值時,都應盡量滿足客戶的需要。在這樣的描述中,銀行家就好比操作系統,資金就是資源,客戶就相當於要申請資源的進程。
銀行家算法是一種最有代表性的避免死鎖的算法。在避免死鎖方法中允許進程動態地申請資源,但系統在進行資源分配之前,應先計算此次分配資源的安全性,若分配不會導致系統進入不安全狀態,則分配,否則等待。為實現銀行家算法,系統必須設置若干數據結構。
安全序列是指一個進程序列{P1,…,Pn}是安全的,即對於每一個進程Pi(1≤i≤n),它以后尚需要的資源量不超過系統當前剩余資源量與所有進程Pj (j < i )當前占有資源量之和。(即在分配過程中,不會出現某一進程后續需要的資源量比其他所有進程及當前剩余資源量總和還大的情況)
注:存在安全序列則系統是安全的,如果不存在則系統不安全,但不安全狀態不一定引起死鎖。
原理過程
系統給當前進程分配資源時,先檢查是否安全:
在滿足當前的進程X資源申請后,是否還能有足夠的資源去滿足下一個距最大資源需求最近的進程(如某進程最大需要5個單位資源,已擁有1個,還尚需4個),若可以滿足,則繼續檢查下一個距最大資源需求最近的進程,若均能滿足所有進程,則表示為安全,可以允許給當前進程X分配其所需的資源申請,否則讓該進程X進入等待。
注:檢查過程中,每擬滿足一個進程,則進行下個檢查時,當前可用資源為回收上一個進程資源的總值,每滿足一個進程表示此進程已結束,資源可回收。
如圖:
算法過程
Available[ ]矩陣數組表示某類資源的可用量
Claim[ i ][ j ]表示進程Pi最大需要Rj類資源的數量
Allocation[ i ][ j ]表示Pi已占有的Rj類資源數量
Need[ i ][ j ]表示Pi尚需Rj類資源的數量
Need[ i ][ j ]=Claim[ i ][ j ]—Allocation[ i ][ j ]
Request[ i ]表示進程Pi進程的申請向量,如 Request[ i ][ j ]=m 表示Pi申請m個Rj類資源
對於當前進程Pi X
(1) 檢查if( Request[ i ][ j ]<=Need[ i ][ j ] ) goto (2)
else error(“進程 i 對資源的申請量大於其說明的最大值 ”);
(2) 檢查 if ( Request[ i ][ j ]<=Available[ j ] ) goto (3)
else wait() ; /注意是等待!即在對后續進程的需求資源判斷中,若出現不符合的則安全檢查結束,當前進程進入等待/
(3) 系統試探地把資源分給Pi 並修改各項屬性值 (具體是否成立,則根據安全檢查的結果)
Available[ j ] =Available[ j ] — Request[ i ][ j ]
Allocation[ i ][ j ]=Allocation[ i ][ j ] +Request[ i ][ j ]
Need[ i ][ j ]=Need[ i ][ j ]— Request[ i ][ j ]
(4) 安全檢查,若檢查結果為安全,則(3)中執行有效,否則分配作廢,使該Pi進程進入等待
檢查算法描述:
向量Free[ j ]表示系統可分配給各進程的Rj類資源數目,初始與當前Available等值
向量Finish[ i ]表示進程Pi在此次檢查中是否被滿足,初始均為false 當有足有資源可分配給進程時,
Finish[ i ]=true, Pi完成並釋放資源(Free[ j ]+=Allocation[ i ][ j ])
- 從進程隊列中找一個能滿足下述條件的進程Pi
①、Finish[ i ]==false,表示資源未分配給Pi進程
②、Need[ i ][ j ] < Free[ j ],表示資源足夠分配給Pi進程
- 當Pi獲得資源后,認為Pi完成,釋放資源
Free[ j ]+=Allocation[ i ][ j ];
Finish[ i ]=true;
goto Step 1);
例:
int trueSum=0, i=0 ; boolean Flag=true;
while( trueSum < P.length-1 && Flag == true ) {
i=i%P.length;
if( Finish[ i ] == false ){
if(Need[ i ][ j ] < Free[ j ]){
Free[ j ]+=Allocation[ i ][ j ];
Finish[ i ]=true;
trueSum++;
i++;
} else {
Flag=false;
}
}
}
if( Flag==false)
檢查不通過,拒絕當前進程X的資源申請
else
檢查通過,允許為當前進程X分配資源
即若可達到Finish[ 0,1,2,......n ] == true 成立則表示系統處於安全狀態
例:
有5個進程{P1,P2,P3,P4,P5} 。4類資源{R1,R2,R3,R4} 各自數量為6、3、4、2
T0時刻各進程分配資源情況如下
T0時刻為安全狀態,存在安全序列{P4,P1,P2,P3,P5} 如下:
注意:銀行家算法在避免死鎖角度上非常有效,但是需要在進程運行前就知道其所需資源的最大值,且進程數也通常不是固定的,因此使用有限,但從思想上可以提供了解,可以轉換地應用在其他地方。
參考文檔:
https://cloud.tencent.com/developer/article/1541513
https://blog.csdn.net/fdoubleman/article/details/97238420