誰還沒經歷過死鎖呢?
之前剛學習多線程時,由于各種鎖的操作不當,經常不經意間程序寫了代碼就發(fā)生了死鎖,不是在灰度測試的時候被測出來,就是在代碼review的時候被提前發(fā)現(xiàn)。
這種死鎖的經歷不知道大家有沒有,不過怎么說都是一個面試高頻題目,面試官是肯定希望你經歷過的,沒經歷過那也得看看某八股文職業(yè)選手的文章裝作經歷過。
那么什么是死鎖呢?為什么會產生死鎖呢?
什么是死鎖
敖丙和小美是公司同事,今天他們參加了兩個不同主題的會議。但是只有一臺筆記本電腦,一個投影儀。敖丙拿了筆記本,小美拿了投影儀。
那么會議開了一半,我發(fā)現(xiàn):不行啊!開會除了筆記本電腦還需要投影給別的同事看啊,而小美在另一個會議室也發(fā)現(xiàn)了,自己只拿個投影儀沒啥用啊,這里連電腦都沒有。
于是,我需要小美的投影儀,小美需要敖丙的電腦,他們都需要對方手里的資源,但是又不能放棄自己所持有的。
所以兩個會議都開不下去了。
就是因為這個原因,讓會議進程耽擱了兩個小時。兩邊的老板都炸了:“ 開會前怎么連這些都沒準備好,還想不想干了?!”
于是老板讓敖丙寫個檢討好好復盤整個事情,以及產生問題的原因。
細心的傻瓜一定發(fā)現(xiàn)了,為什么小美不用寫呢?
當然因為小美跟老板是......親戚呀~
上面的問題其實就是死鎖,我就想著能不能用代碼描述整個過程。
于是在檢討上寫了以下這段代碼:
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兩個線程都需要獲取鎖去訪問各自的臨界區(qū) ,但是它們又分別依賴對方的資源。
于是兩個線程就同時進入了等待對方資源釋放的情況,但是誰都無法釋放。
這就造成了死鎖的狀況。
死鎖排查
但是這僅僅只是一個大概率的猜測,已經知道程序出現(xiàn)了異常,又如何第一時間排查是不是死鎖呢?我繼續(xù)研究了起來。
他通過Java提供的一些檢測方式,進行了快速的定位。
Jps & Jstack
Jps是Jdk自帶的一個工具,可以查看到正在運行的Java進程:
ok,可以看到。DeadLockDemo的進程ID是1884,拿到這個進程ID,再使用jstack命令。
jstack是Java性能排查的利器,主要用來實時跟蹤進程里對應線程的堆棧信息,可以將Jvm進程內的所有線程的調用棧都打印出來。
所以,直接跟蹤1884這個進程ID就行。
果然,可以看到,jstack已經檢測到了死鎖。并且Aobing和xiaomei兩個線程都在互相等待對方的鎖釋放,也就是阻塞狀態(tài)。
從這里,我確認程序發(fā)生了死鎖**。
馬上跑過去對正在和小美喝咖啡的老說說:“ 老板,你看這真的不是我的錯啊,是咱公司資源不夠,發(fā)生了死鎖!我寫個程序都跑不出結果!”
老板道貌儼然地點了點頭?!班?,那你還是得想想怎么解決,一個問題不能連續(xù)犯兩次!”
于是在當天深夜11點,敖丙進行了深刻的自我反思,默默的寫下這篇文章:「一個關于死鎖的故事」。
死鎖的類型
OK,看完了上面的故事,我們回過頭來,繼續(xù)來講關于死鎖的知識。
關于死鎖有幾種類型呢?主要有三種:
- 一般性死鎖:這是最經典的死鎖方式。指的是多線程環(huán)境下每個線程都需要多個資源去執(zhí)行,但是這些資源又分別被不同的線程占有著,這就造成了一種僵持的狀態(tài)。
- 嵌套性死鎖:指的就是鎖的互相嵌套使用。我們上面故事的死鎖類型,其實就屬于嵌套性死鎖。
- 重入性死鎖:指的是多線程環(huán)境下,若當前線程重復調用一個方法則可能因為代碼邏輯里的邊界情況從而導致死鎖。
所以后來Java中無論是Synchronized還是Lock在可重入方面都會維護一個計數(shù)器來記錄當前線程的重入次數(shù),從而進入不同的代碼邏輯,就是為了避免死鎖的發(fā)生。
死鎖原理
那么有的小伙伴就會擔心了:“聽你這么分析,我以后都不敢隨意用它們了,這要是背鍋了可怎么辦!”。
別擔心,死鎖哪有那么容易發(fā)生呢。
你應該問一個問題:程序為什么會出現(xiàn)死鎖,或者說在什么情況下,程序才會出現(xiàn)死鎖。
要產生死鎖,必須保證你的資源要能夠滿足以下條件,并且缺一不可:
- 互斥條件
某資源一次只能一個線程訪問,該資源只要分配給某個線程,其它線程就無法再訪問,直到該線程訪問結束。
- 請求與保持條件
線程在已經占有至少一個資源的情況下還可以繼續(xù)請求占有資源。
- 不可搶占條件
資源若已被其它線程占有,那么想要獲取它就只能等待,不能因為你需要該資源就將其搶占。
- 循環(huán)等待條件
在競爭環(huán)境中存在一個線程等待鏈,使得每個線程都占有上一個線程所需的至少一種資源。
也就是說只有以上四個條件同時滿足,線程才會因為資源分配產生矛盾,死鎖才有可能發(fā)生。
大家可以類比一下,敖丙和小美是不是就處于以上四個條件中呢。
所以說,不要擔心,想要發(fā)生死鎖還是非常不容易滴。
死鎖解除
那當你確定了程序發(fā)生了死鎖,怎么辦呢?
當然是不要慌,先給文章點個贊,收藏一下先,確保以后能找到。
我們剛剛說了,死鎖發(fā)生的情況是要同時滿足互斥、請求與保持、不可剝奪、循環(huán)等待這四個條件,缺一不可。那么我們如果想要解除死鎖,是不是只要將這四個條件的任意一個破壞掉就好了呢?
- 破壞請求與保持條件
請求與保持指線程請求資源的同時必須始終持有資源,所以我們可以在線程開始運行之前,一次性地申請其在整個運行過程中所需的全部資源。直至使用完再釋放。
- 破壞不可搶占條件
想要達到這個目的代表著你要去搶占別的線程已經或正在持有的資源,這對于Synchronized是無能為力的。但是我們可以使用Lock呀!在JDK層面,juc包(java.util.concurrent)提供的Lock可以輕輕松松做到。
- 破壞循環(huán)等待條件
若是每個線程都依賴上一線程所持有的資源,那么整個線程鏈就會像閉環(huán)的貪吃蛇一樣,導致資源無法被釋放。因此就需要某一個線程釋放資源,從而打破循環(huán)。
所以,我們平時的代碼要如何設計才能盡量避免死鎖的發(fā)生呢?
盡量將程序設置為可中斷的
將程序設置為可中斷的,這樣在死鎖環(huán)境下如果某個線程收到中斷請求之后就可以主動地釋放掉手中的資源。
Java多線程中有一個重要的方法interrupt(),這個方法可以請求調用此方法的線程觸發(fā)中斷機制,該線程可以自身決定是否釋放資源。若是已經發(fā)生了死鎖,只要它放棄資源便可打破。
為鎖添加時限
除此之外還可以為嘗試獲取鎖的線程添加一個超時等待時間。若線程在規(guī)定時間內獲取不到鎖則放棄,這樣就可以避免線程無腦請求,同時也會釋放該線程已有的資源,讓其它線程有機會獲取到鎖,可以開放化一個相對封閉的資源環(huán)境。
保持加鎖順序
對于多個線程如果需要對方所持有的鎖,那么就要盡量按照相同的順序加鎖,這樣就能夠避免因為各個線程獲取鎖的順序混亂導致死鎖。
我們再回過頭來看看那個關于死鎖的故事。
經過昨天加班的深刻反思,我重新編寫了這段代碼:
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兩個線程都是先獲取lock1再獲取lock2,這樣子兩個線程誰先獲取到資源,誰就一次性持有資源,直到資源都是釋放完畢再讓下一個線程獲取,避免相互爭奪導致資源混亂,破壞了請求與保持條件。
程序也成功運行結束:
所以我決定在下次開會的時候和小美的會議時間分開。由我先一次性獲取所有資源開啟他的會議,結束后資源再還給小美。
我抱著電腦高興地將這個方案告訴了老板。
第二天,由于和小美的友好配合,兩個會議都愉快的開完了,會議過程非常流暢。老板很開心,決定讓我擔任會議編排委員,并且以后會議室不再購入新設備!
我也高興壞了,這下不僅升職加薪不再是夢,老板和小美的關系也更融洽了呢。
總結
以上就是關于我和小美還有老板的故事,其實生活中死鎖的場景有很多,就像雞生蛋蛋生雞一樣,就是一個典型的死鎖Bug。都說藝術來源于生活,看來Bug也來源于生活,等量代換一下,Bug 不 就 等 于 藝 術?