誰還沒經(jīng)歷過死鎖呢?
之前剛學(xué)習(xí)多線程時(shí),由于各種鎖的操作不當(dāng),經(jīng)常不經(jīng)意間程序?qū)懥舜a就發(fā)生了死鎖,不是在灰度測(cè)試的時(shí)候被測(cè)出來,就是在代碼review的時(shí)候被提前發(fā)現(xiàn)。
這種死鎖的經(jīng)歷不知道大家有沒有,不過怎么說都是一個(gè)面試高頻題目,面試官是肯定希望你經(jīng)歷過的,沒經(jīng)歷過那也得看看某八股文職業(yè)選手的文章裝作經(jīng)歷過。
那么什么是死鎖呢?為什么會(huì)產(chǎn)生死鎖呢?
什么是死鎖
敖丙和小美是公司同事,今天他們參加了兩個(gè)不同主題的會(huì)議。但是只有一臺(tái)筆記本電腦,一個(gè)投影儀。敖丙拿了筆記本,小美拿了投影儀。
那么會(huì)議開了一半,我發(fā)現(xiàn):不行啊!開會(huì)除了筆記本電腦還需要投影給別的同事看啊,而小美在另一個(gè)會(huì)議室也發(fā)現(xiàn)了,自己只拿個(gè)投影儀沒啥用啊,這里連電腦都沒有。
于是,我需要小美的投影儀,小美需要敖丙的電腦,他們都需要對(duì)方手里的資源,但是又不能放棄自己所持有的。
所以兩個(gè)會(huì)議都開不下去了。
就是因?yàn)檫@個(gè)原因,讓會(huì)議進(jìn)程耽擱了兩個(gè)小時(shí)。兩邊的老板都炸了:“ 開會(huì)前怎么連這些都沒準(zhǔn)備好,還想不想干了?!”
于是老板讓敖丙寫個(gè)檢討好好復(fù)盤整個(gè)事情,以及產(chǎn)生問題的原因。
細(xì)心的傻瓜一定發(fā)現(xiàn)了,為什么小美不用寫呢?
當(dāng)然因?yàn)樾∶栏习迨?.....親戚呀~

上面的問題其實(shí)就是死鎖,我就想著能不能用代碼描述整個(gè)過程。
于是在檢討上寫了以下這段代碼:
public?class?DeadLockDemo?{
????public?static?Object?lock1?=?new?Object();?//獲取筆記本電腦
????public?static?Object?lock2?=?new?Object();?//獲取投影儀
????public?static?void?main(String[]?args)?{
????????new?Aobing().start();
????????new?Xiaomei().start();
????}
????private?static?class?Aobing?extends?Thread?{
????????@Override
????????public?void?run()?{
????????????synchronized?(lock1)?{
????????????????System.out.println("Aobing獲取到筆記本電腦");
????????????????try?{
????????????????????Thread.sleep(1000);
????????????????}?catch?(InterruptedException?e)?{
????????????????????System.out.println("Aobing被中斷了!");
????????????????}
????????????????System.out.println("Aobing正在等待投影儀");
????????????????synchronized?(lock2)?{
????????????????????System.out.println("Aobing獲取到投影儀");
????????????????????try?{
????????????????????????Thread.sleep(1000);
????????????????????}?catch?(InterruptedException?e)?{
????????????????????????System.out.println("Aobing被中斷了");
????????????????????}
????????????????}
????????????????System.out.println("Aobing釋放投影儀");
????????????}
????????????System.out.println("Aobing釋放筆記本電腦");
????????}
????}
????private?static?class?Xiaomei?extends?Thread?{
????????@Override
????????public?void?run()?{
????????????synchronized?(lock2)?{
????????????????System.out.println("Xiaomei獲取到投影儀");
????????????????try?{
????????????????????Thread.sleep(1000);
????????????????}?catch?(InterruptedException?e)?{
????????????????????System.out.println("Xiaomei被中斷了!");
????????????????}
????????????????System.out.println("Xiaomei正在等待筆記本電腦");
????????????????synchronized?(lock1)?{
????????????????????System.out.println("Xiaomei獲取到筆記本電腦");
????????????????????try?{
????????????????????????Thread.sleep(1000);
????????????????????}?catch?(InterruptedException?e)?{
????????????????????????System.out.println("Xiaomei被中斷了!");
????????????????????}
????????????????}
????????????????System.out.println("Xiaomei釋放筆記本電腦");
????????????}
????????????System.out.println("Xiaomei釋放投影儀");
????????}
????}
}
從上面程序看出來了,Aobing和Xiaomei兩個(gè)線程都需要獲取鎖去訪問各自的臨界區(qū) ,但是它們又分別依賴對(duì)方的資源。
于是兩個(gè)線程就同時(shí)進(jìn)入了等待對(duì)方資源釋放的情況,但是誰都無法釋放。
這就造成了死鎖的狀況。

死鎖排查
但是這僅僅只是一個(gè)大概率的猜測(cè),已經(jīng)知道程序出現(xiàn)了異常,又如何第一時(shí)間排查是不是死鎖呢?我繼續(xù)研究了起來。
他通過Java提供的一些檢測(cè)方式,進(jìn)行了快速的定位。
Jps & Jstack
Jps是Jdk自帶的一個(gè)工具,可以查看到正在運(yùn)行的Java進(jìn)程:

ok,可以看到。DeadLockDemo的進(jìn)程ID是1884,拿到這個(gè)進(jìn)程ID,再使用jstack命令。
jstack是Java性能排查的利器,主要用來實(shí)時(shí)跟蹤進(jìn)程里對(duì)應(yīng)線程的堆棧信息,可以將Jvm進(jìn)程內(nèi)的所有線程的調(diào)用棧都打印出來。
所以,直接跟蹤1884這個(gè)進(jìn)程ID就行。

果然,可以看到,jstack已經(jīng)檢測(cè)到了死鎖。并且Aobing和xiaomei兩個(gè)線程都在互相等待對(duì)方的鎖釋放,也就是阻塞狀態(tài)。
從這里,我確認(rèn)程序發(fā)生了死鎖**。
馬上跑過去對(duì)正在和小美喝咖啡的老說說:“ 老板,你看這真的不是我的錯(cuò)啊,是咱公司資源不夠,發(fā)生了死鎖!我寫個(gè)程序都跑不出結(jié)果!”

老板道貌儼然地點(diǎn)了點(diǎn)頭。“嗯,那你還是得想想怎么解決,一個(gè)問題不能連續(xù)犯兩次!”
于是在當(dāng)天深夜11點(diǎn),敖丙進(jìn)行了深刻的自我反思,默默的寫下這篇文章:「一個(gè)關(guān)于死鎖的故事」。
死鎖的類型
OK,看完了上面的故事,我們回過頭來,繼續(xù)來講關(guān)于死鎖的知識(shí)。
關(guān)于死鎖有幾種類型呢?主要有三種:
一般性死鎖:這是最經(jīng)典的死鎖方式。指的是多線程環(huán)境下每個(gè)線程都需要多個(gè)資源去執(zhí)行,但是這些資源又分別被不同的線程占有著,這就造成了一種僵持的狀態(tài)。

嵌套性死鎖:指的就是鎖的互相嵌套使用。我們上面故事的死鎖類型,其實(shí)就屬于嵌套性死鎖。

重入性死鎖:指的是多線程環(huán)境下,若當(dāng)前線程重復(fù)調(diào)用一個(gè)方法則可能因?yàn)榇a邏輯里的邊界情況從而導(dǎo)致死鎖。

所以后來Java中無論是Synchronized還是Lock在可重入方面都會(huì)維護(hù)一個(gè)計(jì)數(shù)器來記錄當(dāng)前線程的重入次數(shù),從而進(jìn)入不同的代碼邏輯,就是為了避免死鎖的發(fā)生。
死鎖原理
那么有的小伙伴就會(huì)擔(dān)心了:“聽你這么分析,我以后都不敢隨意用它們了,這要是背鍋了可怎么辦!”。
別擔(dān)心,死鎖哪有那么容易發(fā)生呢。
你應(yīng)該問一個(gè)問題:程序?yàn)槭裁磿?huì)出現(xiàn)死鎖,或者說在什么情況下,程序才會(huì)出現(xiàn)死鎖。
要產(chǎn)生死鎖,必須保證你的資源要能夠滿足以下條件,并且缺一不可:
互斥條件
某資源一次只能一個(gè)線程訪問,該資源只要分配給某個(gè)線程,其它線程就無法再訪問,直到該線程訪問結(jié)束。

請(qǐng)求與保持條件
線程在已經(jīng)占有至少一個(gè)資源的情況下還可以繼續(xù)請(qǐng)求占有資源。

不可搶占條件
資源若已被其它線程占有,那么想要獲取它就只能等待,不能因?yàn)槟阈枰撡Y源就將其搶占。

循環(huán)等待條件
在競(jìng)爭(zhēng)環(huán)境中存在一個(gè)線程等待鏈,使得每個(gè)線程都占有上一個(gè)線程所需的至少一種資源。

也就是說只有以上四個(gè)條件同時(shí)滿足,線程才會(huì)因?yàn)橘Y源分配產(chǎn)生矛盾,死鎖才有可能發(fā)生。
大家可以類比一下,敖丙和小美是不是就處于以上四個(gè)條件中呢。
所以說,不要擔(dān)心,想要發(fā)生死鎖還是非常不容易滴。
死鎖解除
那當(dāng)你確定了程序發(fā)生了死鎖,怎么辦呢?
當(dāng)然是不要慌,先給文章點(diǎn)個(gè)贊,收藏一下先,確保以后能找到。

我們剛剛說了,死鎖發(fā)生的情況是要同時(shí)滿足互斥、請(qǐng)求與保持、不可剝奪、循環(huán)等待這四個(gè)條件,缺一不可。那么我們?nèi)绻胍獬梨i,是不是只要將這四個(gè)條件的任意一個(gè)破壞掉就好了呢?
破壞請(qǐng)求與保持條件
請(qǐng)求與保持指線程請(qǐng)求資源的同時(shí)必須始終持有資源,所以我們可以在線程開始運(yùn)行之前,一次性地申請(qǐng)其在整個(gè)運(yùn)行過程中所需的全部資源。直至使用完再釋放。
破壞不可搶占條件
想要達(dá)到這個(gè)目的代表著你要去搶占別的線程已經(jīng)或正在持有的資源,這對(duì)于Synchronized是無能為力的。但是我們可以使用Lock呀!在JDK層面,juc包(java.util.concurrent)提供的Lock可以輕輕松松做到。
破壞循環(huán)等待條件
若是每個(gè)線程都依賴上一線程所持有的資源,那么整個(gè)線程鏈就會(huì)像閉環(huán)的貪吃蛇一樣,導(dǎo)致資源無法被釋放。因此就需要某一個(gè)線程釋放資源,從而打破循環(huán)。
所以,我們平時(shí)的代碼要如何設(shè)計(jì)才能盡量避免死鎖的發(fā)生呢?
盡量將程序設(shè)置為可中斷的
將程序設(shè)置為可中斷的,這樣在死鎖環(huán)境下如果某個(gè)線程收到中斷請(qǐng)求之后就可以主動(dòng)地釋放掉手中的資源。
Java多線程中有一個(gè)重要的方法interrupt(),這個(gè)方法可以請(qǐng)求調(diào)用此方法的線程觸發(fā)中斷機(jī)制,該線程可以自身決定是否釋放資源。若是已經(jīng)發(fā)生了死鎖,只要它放棄資源便可打破。
為鎖添加時(shí)限
除此之外還可以為嘗試獲取鎖的線程添加一個(gè)超時(shí)等待時(shí)間。若線程在規(guī)定時(shí)間內(nèi)獲取不到鎖則放棄,這樣就可以避免線程無腦請(qǐng)求,同時(shí)也會(huì)釋放該線程已有的資源,讓其它線程有機(jī)會(huì)獲取到鎖,可以開放化一個(gè)相對(duì)封閉的資源環(huán)境。
保持加鎖順序
對(duì)于多個(gè)線程如果需要對(duì)方所持有的鎖,那么就要盡量按照相同的順序加鎖,這樣就能夠避免因?yàn)楦鱾€(gè)線程獲取鎖的順序混亂導(dǎo)致死鎖。
我們?cè)倩剡^頭來看看那個(gè)關(guān)于死鎖的故事。
經(jīng)過昨天加班的深刻反思,我重新編寫了這段代碼:
public?class?DeadLockDemo?{
????public?static?Object?lock1?=?new?Object();??//獲取筆記本電腦
????public?static?Object?lock2?=?new?Object();??//獲取投影儀
????public?static?void?main(String[]?args)?{
????????new?Thread1().start();
????????new?Thread2().start();
????}
????private?static?class?Thread1?extends?Thread?{
????????@Override
????????public?void?run()?{
????????????synchronized?(lock1)?{
????????????????System.out.println("Aobing獲取到筆記本電腦");
????????????????try?{
????????????????????Thread.sleep(1000);
????????????????}?catch?(InterruptedException?e)?{
????????????????????System.out.println("Aobing被中斷了!");
????????????????}
????????????????System.out.println("Aobing正在等待投影儀");
????????????????synchronized?(lock2)?{
????????????????????System.out.println("Aobing獲取到投影儀");
????????????????????try?{
????????????????????????Thread.sleep(1000);
????????????????????}?catch?(InterruptedException?e)?{
????????????????????????System.out.println("Aobing被中斷了");
????????????????????}
????????????????}
????????????????System.out.println("Aobing釋放投影儀");
????????????}
????????????System.out.println("Aobing釋放筆記本電腦");
????????}
????}
????private?static?class?Thread2?extends?Thread?{
????????@Override
????????public?void?run()?{
????????????synchronized?(lock1)?{
????????????????System.out.println("Xiaomei獲取到筆記本電腦");
????????????????try?{
????????????????????Thread.sleep(1000);
????????????????}?catch?(InterruptedException?e)?{
????????????????????System.out.println("Xiaomei被中斷了!");
????????????????}
????????????????System.out.println("Xiaomei正在等待投影儀");
????????????????synchronized?(lock2){
????????????????????System.out.println("Xiaomei獲取到了投影儀");
????????????????????try?{
????????????????????????Thread.sleep(1000);
????????????????????}?catch?(InterruptedException?e)?{
????????????????????????System.out.println("Xiaomei被中斷了!");
????????????????????}
????????????????}
????????????????System.out.println("Xiaomei釋放投影儀");
????????????}
????????????System.out.println("Xiaomei釋放筆記本電腦");
????????}
????}
}
這段代碼和一開始的有什么區(qū)別呢?這次它們獲取鎖的順序是相同的。
Aobing和Xiaomei兩個(gè)線程都是先獲取lock1再獲取lock2,這樣子兩個(gè)線程誰先獲取到資源,誰就一次性持有資源,直到資源都是釋放完畢再讓下一個(gè)線程獲取,避免相互爭(zhēng)奪導(dǎo)致資源混亂,破壞了請(qǐng)求與保持條件。
程序也成功運(yùn)行結(jié)束:

所以我決定在下次開會(huì)的時(shí)候和小美的會(huì)議時(shí)間分開。由我先一次性獲取所有資源開啟他的會(huì)議,結(jié)束后資源再還給小美。
我抱著電腦高興地將這個(gè)方案告訴了老板。
第二天,由于和小美的友好配合,兩個(gè)會(huì)議都愉快的開完了,會(huì)議過程非常流暢。老板很開心,決定讓我擔(dān)任會(huì)議編排委員,并且以后會(huì)議室不再購入新設(shè)備!

我也高興壞了,這下不僅升職加薪不再是夢(mèng),老板和小美的關(guān)系也更融洽了呢。

總結(jié)
以上就是關(guān)于我和小美還有老板的故事,其實(shí)生活中死鎖的場(chǎng)景有很多,就像雞生蛋蛋生雞一樣,就是一個(gè)典型的死鎖Bug。都說藝術(shù)來源于生活,看來Bug也來源于生活,等量代換一下,Bug 不 就 等 于 藝 術(shù)?
我這該死的才華啊,大家覺得有點(diǎn)東西的,可以評(píng)論區(qū)打下“有點(diǎn)東西”。
我是敖丙,感謝各位的三連,你知道的越多,你不知道的越多,我們下期見。
