1伴箩、死鎖是什么?
首先要知道鄙漏,死鎖一定發(fā)生在并發(fā)場(chǎng)景中嗤谚。為了保證線程安全,有時(shí)會(huì)給程序使用各種能保證并發(fā)安全的工具怔蚌,尤其是鎖巩步,但是如果在使用過(guò)程中處理不得當(dāng),就有可能會(huì)導(dǎo)致發(fā)生死鎖的情況桦踊。
死鎖是一種狀態(tài)椅野,當(dāng)兩個(gè)(或多個(gè))線程(或進(jìn)程)相互持有對(duì)方所需要的資源,卻又都不主動(dòng)釋放自己手中所持有的資源钞钙,導(dǎo)致大家都獲取不到自己想要的資源鳄橘,所有相關(guān)的線程(或進(jìn)程)都無(wú)法繼續(xù)往下執(zhí)行,在未改變這種狀態(tài)之前都不能向前推進(jìn)芒炼,就把這種狀態(tài)稱為死鎖狀態(tài)瘫怜。通俗的講,死鎖就是兩個(gè)或多個(gè)線程(或進(jìn)程)被無(wú)限期地阻塞本刽,相互等待對(duì)方手中資源的一種狀態(tài)鲸湃。
發(fā)生死鎖的例子:
/**
* 描述: 必定死鎖的情況
*/
public class MustDeadLock implements Runnable {
public int flag;
static Object o1 = new Object();
static Object o2 = new Object();
public void run() {
System.out.println("線程"+Thread.currentThread().getName() + "的flag為" + flag);
if (flag == 1) {
synchronized (o1) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println("線程1獲得了兩把鎖");
}
}
}
if (flag == 2) {
synchronized (o2) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println("線程2獲得了兩把鎖");
}
}
}
}
public static void main(String[] argv) {
MustDeadLock r1 = new MustDeadLock();
MustDeadLock r2 = new MustDeadLock();
r1.flag = 1;
r2.flag = 2;
Thread t1 = new Thread(r1, "t1");
Thread t2 = new Thread(r2, "t2");
t1.start();
t2.start();
}
}
兩個(gè)線程發(fā)生死鎖的情況:
多個(gè)線程造成死鎖的情況:
2、死鎖的影響
死鎖的影響在不同系統(tǒng)中是不一樣的子寓,影響的大小一部分取決于當(dāng)前這個(gè)系統(tǒng)或者環(huán)境對(duì)死鎖的處理能力暗挑。
(1)數(shù)據(jù)庫(kù)中
例如,在數(shù)據(jù)庫(kù)系統(tǒng)軟件的設(shè)計(jì)中斜友,考慮了監(jiān)測(cè)死鎖以及從死鎖中恢復(fù)的情況炸裆。在執(zhí)行一個(gè)事務(wù)的時(shí)候可能需要獲取多把鎖,并一直持有這些鎖直到事務(wù)完成鲜屏。在某個(gè)事務(wù)中持有的鎖可能在其他事務(wù)中也需要烹看,因此在兩個(gè)事務(wù)之間有可能發(fā)生死鎖的情況国拇,一旦發(fā)生了死鎖,如果沒(méi)有外部干涉惯殊,那么兩個(gè)事務(wù)就會(huì)永遠(yuǎn)的等待下去酱吝。
但數(shù)據(jù)庫(kù)系統(tǒng)不會(huì)放任這種情況發(fā)生,當(dāng)數(shù)據(jù)庫(kù)檢測(cè)到這一組事務(wù)發(fā)生了死鎖時(shí)土思,根據(jù)策略的不同务热,可能會(huì)選擇放棄某一個(gè)事務(wù),被放棄的事務(wù)就會(huì)釋放掉它所持有的鎖己儒,從而使其他的事務(wù)繼續(xù)順利進(jìn)行崎岂。此時(shí)程序可以重新執(zhí)行被強(qiáng)行終止的事務(wù),而這個(gè)事務(wù)現(xiàn)在就可以順利執(zhí)行了址愿,因?yàn)樗懈?jìng)爭(zhēng)資源的事務(wù)都已經(jīng)在剛才執(zhí)行完畢该镣,并且釋放資源了。
(2)JVM 中
在 JVM 中發(fā)生了死鎖响谓,JVM 并不會(huì)自動(dòng)進(jìn)行處理,所以一旦死鎖發(fā)生省艳,就會(huì)陷入無(wú)窮的等待娘纷。
(3)幾率不高但危害大
死鎖的問(wèn)題和其他的并發(fā)安全問(wèn)題一樣,是概率性的跋炕,即使存在發(fā)生死鎖的可能性赖晶,也并不是 100% 會(huì)發(fā)生的。如果每個(gè)鎖的持有時(shí)間很短辐烂,那么發(fā)生沖突的概率就很低遏插,所以死鎖發(fā)生的概率也很低。但是在線上系統(tǒng)里纠修,可能每天有幾千萬(wàn)次的“獲取鎖”胳嘲、“釋放鎖”操作,在巨量的次數(shù)面前扣草,整個(gè)系統(tǒng)發(fā)生問(wèn)題的幾率就會(huì)被放大了牛,只要有某幾次操作是有風(fēng)險(xiǎn)的,就可能會(huì)導(dǎo)致死鎖的發(fā)生辰妙。
因?yàn)樗梨i“不一定會(huì)發(fā)生”的特點(diǎn)鹰祸,導(dǎo)致提前找出死鎖成為了一個(gè)難題。壓力測(cè)試雖然可以檢測(cè)出一部分可能發(fā)生死鎖的情況密浑,但是并不足以完全模擬真實(shí)蛙婴、長(zhǎng)期運(yùn)行的場(chǎng)景,因此沒(méi)有辦法把所有潛在可能發(fā)生死鎖的代碼都找出來(lái)尔破。
一旦發(fā)生了死鎖街图,根據(jù)發(fā)生死鎖的線程的職責(zé)不同浇衬,就可能會(huì)造成子系統(tǒng)崩潰、性能降低甚至整個(gè)系統(tǒng)崩潰等各種不良后果台夺。而且死鎖往往發(fā)生在高并發(fā)径玖、高負(fù)載的情況下,因?yàn)榭赡軙?huì)直接影響到很多用戶颤介,造成一系列的問(wèn)題梳星。
3、發(fā)生死鎖的 4 個(gè)必要條件
第 1 個(gè)叫互斥條件滚朵,它的意思是每個(gè)資源每次只能被一個(gè)線程(或進(jìn)程冤灾,下同)使用,為什么資源不能同時(shí)被多個(gè)線程或進(jìn)程使用呢辕近?這是因?yàn)槿绻總€(gè)人都可以拿到想要的資源韵吨,那就不需要等待,所以是不可能發(fā)生死鎖的移宅。
第 2 個(gè)是請(qǐng)求與保持條件归粉,它是指當(dāng)一個(gè)線程因請(qǐng)求資源而阻塞時(shí),則需對(duì)已獲得的資源保持不放漏峰。如果在請(qǐng)求資源時(shí)阻塞了糠悼,并且會(huì)自動(dòng)釋放手中資源(例如鎖)的話,那別人自然就能拿到我剛才釋放的資源浅乔,也就不會(huì)形成死鎖倔喂。
第 3 個(gè)是不剝奪條件,它是指線程已獲得的資源靖苇,在未使用完之前席噩,不會(huì)被強(qiáng)行剝奪。比如我們?cè)谏弦徽n時(shí)中介紹的數(shù)據(jù)庫(kù)的例子贤壁,它就有可能去強(qiáng)行剝奪某一個(gè)事務(wù)所持有的資源悼枢,這樣就不會(huì)發(fā)生死鎖了。所以要想發(fā)生死鎖芯砸,必須滿足不剝奪條件萧芙,也就是說(shuō)當(dāng)現(xiàn)在的線程獲得了某一個(gè)資源后,別人就不能來(lái)剝奪這個(gè)資源假丧,這才有可能形成死鎖双揪。
第 4 個(gè)是循環(huán)等待條件,只有若干線程之間形成一種頭尾相接的循環(huán)等待資源關(guān)系時(shí)包帚,才有可能形成死鎖渔期,比如在兩個(gè)線程之間,這種“循環(huán)等待”就意味著它們互相持有對(duì)方所需的資源、互相等待疯趟;而在三個(gè)或更多線程中拘哨,則需要形成環(huán)路,例如依次請(qǐng)求下一個(gè)線程已持有的資源等信峻。
4倦青、如何用命令行和代碼定位死鎖?
(1)命令:jstack 定位
它能看到 Java 線程的一些相關(guān)信息盹舞。如果是比較明顯的死鎖關(guān)系产镐,那么這個(gè)工具就可以直接檢測(cè)出來(lái);如果死鎖不明顯踢步,那么它無(wú)法直接檢測(cè)出來(lái)癣亚,不過(guò)我們也可以借此來(lái)分析線程狀態(tài),進(jìn)而就可以發(fā)現(xiàn)鎖的相互依賴關(guān)系获印,所以這也是很有利于找到死鎖的方式述雾。
首先,運(yùn)行一下必然發(fā)生死鎖的 MustDeadLock 類兼丰;
然后玻孟,然后打開(kāi)終端,執(zhí)行 jps 這個(gè)命令鳍征,就可以查看到當(dāng)前 Java 程序的 pid取募,執(zhí)行結(jié)果如下:
56402 MustDeadLock
56403 Launcher
56474 Jps
55051 KotlinCompileDaemon
可以看到第一行是 MustDeadLock 這類的 pid 56402;繼續(xù)執(zhí)行下一個(gè)命令 jstack 56402蟆技;最后它會(huì)打印出很多信息,就包含了線程獲取鎖的信息斗忌,比如哪個(gè)線程獲取哪個(gè)鎖质礼,它獲得的鎖是在哪個(gè)語(yǔ)句中獲得的,它正在等待或者持有的鎖是什么等织阳。截取一部分和死鎖相關(guān)的有用信息眶蕉,展示如下:
Found one Java-level deadlock:
=============================
"t2":
waiting to lock monitor 0x00007fa06c004a18 (object 0x000000076adabaf0, a java.lang.Object),
which is held by "t1"
"t1":
waiting to lock monitor 0x00007fa06c007358 (object 0x000000076adabb00, a java.lang.Object),
which is held by "t2"
Java stack information for the threads listed above:
===================================================
"t2":
at lesson67.MustDeadLock.run(MustDeadLock.java:31)
- waiting to lock <0x000000076adabaf0> (a java.lang.Object)
- locked <0x000000076adabb00> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:748)
"t1":
at lesson67.MustDeadLock.run(MustDeadLock.java:19)
- waiting to lock <0x000000076adabb00> (a java.lang.Object)
- locked <0x000000076adabaf0> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock
可以看出,jstack 工具不但找到了死鎖唧躲,甚至還把哪個(gè)線程造挽、想要獲取哪個(gè)鎖、形成什么樣的環(huán)路都告訴我們了弄痹,當(dāng)有了這樣的信息之后饭入,死鎖就非常容易定位了,接下來(lái)就可以進(jìn)一步修改代碼肛真,來(lái)避免死鎖了谐丢。
(2)代碼:ThreadMXBean 定位
在前面 MustDeadLock 類的 main 方法中加入如下代碼:
public static void main(String[] argv) throws InterruptedException {
MustDeadLock r1 = new MustDeadLock();
MustDeadLock r2 = new MustDeadLock();
r1.flag = 1;
r2.flag = 2;
Thread t1 = new Thread(r1, "t1");
Thread t2 = new Thread(r2, "t2");
t1.start();
t2.start();
Thread.sleep(1000);
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
if (deadlockedThreads != null && deadlockedThreads.length > 0) {
for (int i = 0; i < deadlockedThreads.length; i++) {
ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThreads[i]);
System.out.println("線程id為"+threadInfo.getThreadId()+
",線程名為" + threadInfo.getThreadName()+"的線程已經(jīng)發(fā)生死鎖,
需要的鎖正被線程"+threadInfo.getLockOwnerName()+"持有。");
}
通過(guò) ThreadMXBean 的 findDeadlockedThreads 方法乾忱,可以獲取到一個(gè) deadlockedThreads 的數(shù)組讥珍,然后進(jìn)行判斷,當(dāng)這個(gè)數(shù)組不為空且長(zhǎng)度大于 0 的時(shí)候窄瘟,逐個(gè)打印出對(duì)應(yīng)的線程信息衷佃。比如打印出了線程 id,也打印出了線程名蹄葱,同時(shí)打印出了它所需要的那把鎖正被哪個(gè)線程所持有氏义,那么這一部分代碼的運(yùn)行結(jié)果如下。
t1 flag = 1
t2 flag = 2
線程 id 為 12新蟆,線程名為 t2 的線程已經(jīng)發(fā)生死鎖觅赊,需要的鎖正被線程 t1 持有。
線程 id 為 11琼稻,線程名為 t1 的線程已經(jīng)發(fā)生死鎖吮螺,需要的鎖正被線程 t2 持有。
5帕翻、有哪些解決死鎖問(wèn)題的策略鸠补?
5.1 線上發(fā)生死鎖應(yīng)該怎么辦?
如果線上發(fā)生死鎖問(wèn)題嘀掸,為了盡快減小損失紫岩,最好的辦法是保存 JVM 信息、日志等“案發(fā)現(xiàn)場(chǎng)”的數(shù)據(jù)睬塌,然后立刻重啟服務(wù)泉蝌,來(lái)嘗試修復(fù)死鎖。為什么說(shuō)重啟服務(wù)能解決這個(gè)問(wèn)題呢揩晴?因?yàn)榘l(fā)生死鎖往往要有很多前提條件的勋陪,并且當(dāng)并發(fā)度足夠高的時(shí)候才有可能會(huì)發(fā)生死鎖,所以重啟后再次立刻發(fā)生死鎖的幾率并不是很大硫兰,當(dāng)我們重啟服務(wù)器之后诅愚,就可以暫時(shí)保證線上服務(wù)的可用,然后利用剛才保存過(guò)的案發(fā)現(xiàn)場(chǎng)的信息劫映,排查死鎖违孝、修改代碼,最終重新發(fā)布泳赋。
5.2 常見(jiàn)修復(fù)策略
(1)避免策略
避免策略最主要的思路就是雌桑,優(yōu)化代碼邏輯,從根本上消除發(fā)生死鎖的可能性摹蘑。通常而言筹燕,發(fā)生死鎖的一個(gè)主要原因是順序相反的去獲取不同的鎖。因此就通過(guò)調(diào)整鎖的獲取順序來(lái)避免死鎖。
(2)檢測(cè)與恢復(fù)策略
檢測(cè)與恢復(fù)策略先允許系統(tǒng)發(fā)生死鎖撒踪,然后再解除过咬。例如系統(tǒng)可以在每次調(diào)用鎖的時(shí)候,都記錄下來(lái)調(diào)用信息制妄,形成一個(gè)“鎖的調(diào)用鏈路圖”掸绞,然后隔一段時(shí)間就用死鎖檢測(cè)算法來(lái)檢測(cè)一下,搜索這個(gè)圖中是否存在環(huán)路耕捞,一旦發(fā)生死鎖衔掸,就可以用死鎖恢復(fù)機(jī)制,比如剝奪某一個(gè)資源俺抽,來(lái)解開(kāi)死鎖敞映,進(jìn)行恢復(fù)。
在檢測(cè)到死鎖發(fā)生后磷斧,如何解開(kāi)死鎖呢振愿?
方法1——線程終止
第一種解開(kāi)死鎖的方法是線程(或進(jìn)程,下同)終止弛饭,系統(tǒng)會(huì)逐個(gè)去終止已經(jīng)陷入死鎖的線程冕末,線程被終止,同時(shí)釋放資源侣颂,這樣死鎖就會(huì)被解開(kāi)档桃。
終止是需要講究順序的,一般有以下幾個(gè)考量指標(biāo):
- 優(yōu)先級(jí):一般來(lái)說(shuō)憔晒,終止時(shí)會(huì)考慮到線程或者進(jìn)程的優(yōu)先級(jí)藻肄,先終止優(yōu)先級(jí)低的線程。
- 已占用資源拒担、還需要的資源:如果某線程已經(jīng)占有了一大堆資源仅炊,只需要最后一點(diǎn)點(diǎn)資源就可以順利完成任務(wù),那么系統(tǒng)可能就不會(huì)優(yōu)先選擇終止這樣的線程澎蛛,會(huì)選擇終止別的線程來(lái)優(yōu)先促成該線程的完成。
- 已經(jīng)運(yùn)行時(shí)間:當(dāng)前這個(gè)線程已經(jīng)運(yùn)行了很多個(gè)小時(shí)蜕窿,甚至很多天了谋逻,很快就能完成任務(wù)了,那么終止這個(gè)線程可能不是一個(gè)明智的選擇桐经,可以讓那些剛剛開(kāi)始運(yùn)行的線程終止毁兆,并在之后把它們重新啟動(dòng)起來(lái),這樣成本更低阴挣。
方法2——資源搶占
第二個(gè)解開(kāi)死鎖的方法就是資源搶占气堕。其實(shí),不需要把整個(gè)的線程終止,而是只需要把它已經(jīng)獲得的資源進(jìn)行剝奪茎芭,比如讓線程回退幾步揖膜、 釋放資源,這樣一來(lái)就不用終止掉整個(gè)線程了梅桩,這樣造成的后果會(huì)比剛才終止整個(gè)線程的后果更小一些壹粟,成本更低。
當(dāng)然這種方式也有一個(gè)缺點(diǎn)宿百,那就是如果算法不好的話趁仙,搶占的那個(gè)線程可能一直是同一個(gè)線程,就會(huì)造成線程饑餓垦页。也就是說(shuō)雀费,這個(gè)線程一直被剝奪它已經(jīng)得到的資源,那么它就長(zhǎng)期得不到運(yùn)行痊焊。
(3)鴕鳥(niǎo)策略
鴕鳥(niǎo)策略的意思就是盏袄,如果系統(tǒng)發(fā)生死鎖的概率不高,并且一旦發(fā)生其后果不是特別嚴(yán)重的話宋光,就可以選擇先忽略它貌矿。直到死鎖發(fā)生的時(shí)候,再人工修復(fù)罪佳,比如重啟服務(wù)逛漫,這并不是不可以的。如果系統(tǒng)用的人比較少赘艳,比如是內(nèi)部的系統(tǒng)酌毡,那么在并發(fā)量極低的情況下,它可能幾年都不會(huì)發(fā)生死鎖蕾管。對(duì)此考慮到投入產(chǎn)出比枷踏,自然也沒(méi)有必要去對(duì)死鎖問(wèn)題進(jìn)行特殊的處理,這是需要根據(jù)業(yè)務(wù)場(chǎng)景進(jìn)行合理選擇的掰曾。