簡書江溢Jonny,轉(zhuǎn)載請(qǐng)注明原創(chuàng)出處,謝謝!
關(guān)注我的公眾號(hào),獲得更多干貨~

背景
這個(gè)話題是源自筆者以前跟人的一次技術(shù)討論,“你是怎么發(fā)現(xiàn)死鎖的并且是如何預(yù)防、如何解決的?”以前聽到的這個(gè)問題的時(shí)候,雖然腦海里也有一些思路,但是都是不夠系統(tǒng)化的東西。直到最近親身經(jīng)歷一次死鎖,才做了這么一次集中的思路整理,撰錄以下文字。希望對(duì)同樣問題的同學(xué)有所幫助。
死鎖定義
首先我們先來看看死鎖的定義:“死鎖是指兩個(gè)或兩個(gè)以上的進(jìn)程在執(zhí)行過程中,由于競爭資源或者由于彼此通信而造成的一種阻塞的現(xiàn)象,若無外力作用,它們都將無法推進(jìn)下去?!蹦敲次覀儞Q一個(gè)更加規(guī)范的定義:“集合中的每一個(gè)進(jìn)程都在等待只能由本集合中的其他進(jìn)程才能引發(fā)的事件,那么該組進(jìn)程是死鎖的?!?/p>
競爭的資源可以是:鎖、網(wǎng)絡(luò)連接、通知事件,磁盤、帶寬,以及一切可以被稱作“資源”的東西。
舉個(gè)栗子
上面的內(nèi)容可能有些抽象,因此我們舉個(gè)例子來描述,如果此時(shí)有一個(gè)線程A,按照先鎖a再獲得鎖b的的順序獲得鎖,而在此同時(shí)又有另外一個(gè)線程B,按照先鎖b再鎖a的順序獲得鎖。如下圖所示:

我們用一段代碼來模擬上述過程:
public static void main(String[] args) {
final Object a = new Object();
final Object b = new Object();
Thread threadA = new Thread(new Runnable() {
public void run() {
synchronized (a) {
try {
System.out.println("now i in threadA-locka");
Thread.sleep(1000l);
synchronized (b) {
System.out.println("now i in threadA-lockb");
}
} catch (Exception e) {
// ignore
}
}
}
});
Thread threadB = new Thread(new Runnable() {
public void run() {
synchronized (b) {
try {
System.out.println("now i in threadB-lockb");
Thread.sleep(1000l);
synchronized (a) {
System.out.println("now i in threadB-locka");
}
} catch (Exception e) {
// ignore
}
}
}
});
threadA.start();
threadB.start();
}
程序執(zhí)行結(jié)果如下:

很明顯,程序執(zhí)行停滯了。
死鎖檢測(cè)
在這里,我將介紹兩種死鎖檢測(cè)工具
1、Jstack命令
jstack是java虛擬機(jī)自帶的一種堆棧跟蹤工具。jstack用于打印出給定的java進(jìn)程ID或core file或遠(yuǎn)程調(diào)試服務(wù)的Java堆棧信息。
Jstack工具可以用于生成java虛擬機(jī)當(dāng)前時(shí)刻的線程快照。線程快照是當(dāng)前java虛擬機(jī)內(nèi)每一條線程正在執(zhí)行的方法堆棧的集合,生成線程快照的主要目的是定位線程出現(xiàn)長時(shí)間停頓的原因,如線程間死鎖、死循環(huán)、請(qǐng)求外部資源導(dǎo)致的長時(shí)間等待等。 線程出現(xiàn)停頓的時(shí)候通過jstack來查看各個(gè)線程的調(diào)用堆棧,就可以知道沒有響應(yīng)的線程到底在后臺(tái)做什么事情,或者等待什么資源。
首先,我們通過jps確定當(dāng)前執(zhí)行任務(wù)的進(jìn)程號(hào):
jonny@~$ jps
597
1370 JConsole
1362 AppMain
1421 Jps
1361 Launcher
可以確定任務(wù)進(jìn)程號(hào)是1362,然后執(zhí)行jstack命令查看當(dāng)前進(jìn)程堆棧信息:
jonny@~$ jstack -F 1362
Attaching to process ID 1362, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 23.21-b01
Deadlock Detection:
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock Monitor@0x00007fea1900f6b8 (Object@0x00000007efa684c8, a java/lang/Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock Monitor@0x00007fea1900ceb0 (Object@0x00000007efa684d8, a java/lang/Object),
which is held by "Thread-1"
Found a total of 1 deadlock.
可以看到,進(jìn)程的確存在死鎖,兩個(gè)線程分別在等待對(duì)方持有的Object對(duì)象
2、JConsole工具
Jconsole是JDK自帶的監(jiān)控工具,在JDK/bin目錄下可以找到。它用于連接正在運(yùn)行的本地或者遠(yuǎn)程的JVM,對(duì)運(yùn)行在Java應(yīng)用程序的資源消耗和性能進(jìn)行監(jiān)控,并畫出大量的圖表,提供強(qiáng)大的可視化界面。而且本身占用的服務(wù)器內(nèi)存很小,甚至可以說幾乎不消耗。
我們?cè)诿钚兄星萌雑console命令,會(huì)自動(dòng)彈出以下對(duì)話框,選擇進(jìn)程1362,并點(diǎn)擊“鏈接”

進(jìn)入所檢測(cè)的進(jìn)程后,選擇“線程”選項(xiàng)卡,并點(diǎn)擊“檢測(cè)死鎖”

可以看到以下畫面:

可以看到進(jìn)程中存在死鎖。
以上例子我都是用synchronized關(guān)鍵詞實(shí)現(xiàn)的死鎖,如果讀者用ReentrantLock制造一次死鎖,再次使用死鎖檢測(cè)工具,也同樣能檢測(cè)到死鎖,不過顯示的信息將會(huì)更加豐富,有興趣的讀者可以自己嘗試一下。
死鎖預(yù)防
如果一個(gè)線程每次只能獲得一個(gè)鎖,那么就不會(huì)產(chǎn)生鎖順序的死鎖。雖然不算非?,F(xiàn)實(shí),但是也非常正確(一個(gè)問題的最好解決辦法就是,這個(gè)問題恰好不會(huì)出現(xiàn))。不過關(guān)于死鎖的預(yù)防,這里有以下幾種方案:
1、以確定的順序獲得鎖
如果必須獲取多個(gè)鎖,那么在設(shè)計(jì)的時(shí)候需要充分考慮不同線程之前獲得鎖的順序。按照上面的例子,兩個(gè)線程獲得鎖的時(shí)序圖如下:

如果此時(shí)把獲得鎖的時(shí)序改成:

那么死鎖就永遠(yuǎn)不會(huì)發(fā)生。
針對(duì)兩個(gè)特定的鎖,開發(fā)者可以嘗試按照鎖對(duì)象的hashCode值大小的順序,分別獲得兩個(gè)鎖,這樣鎖總是會(huì)以特定的順序獲得鎖,那么死鎖也不會(huì)發(fā)生。

問題變得更加復(fù)雜一些,如果此時(shí)有多個(gè)線程,都在競爭不同的鎖,簡單按照鎖對(duì)象的hashCode進(jìn)行排序(單純按照hashCode順序排序會(huì)出現(xiàn)“環(huán)路等待”),可能就無法滿足要求了,這個(gè)時(shí)候開發(fā)者可以使用銀行家算法,所有的鎖都按照特定的順序獲取,同樣可以防止死鎖的發(fā)生,該算法在這里就不再贅述了,有興趣的可以自行了解一下。
2、超時(shí)放棄
當(dāng)使用synchronized關(guān)鍵詞提供的內(nèi)置鎖時(shí),只要線程沒有獲得鎖,那么就會(huì)永遠(yuǎn)等待下去,然而Lock接口提供了boolean tryLock(long time, TimeUnit unit) throws InterruptedException方法,該方法可以按照固定時(shí)長等待鎖,因此線程可以在獲取鎖超時(shí)以后,主動(dòng)釋放之前已經(jīng)獲得的所有的鎖。通過這種方式,也可以很有效地避免死鎖。
還是按照之前的例子,時(shí)序圖如下:

其他形式的死鎖
我們?cè)賮砘仡櫼幌滤梨i的定義,“死鎖是指兩個(gè)或兩個(gè)以上的進(jìn)程在執(zhí)行過程中,由于競爭資源或者由于彼此通信而造成的一種阻塞的現(xiàn)象,若無外力作用,它們都將無法推進(jìn)下去。”
死鎖條件里面的競爭資源,可以是線程池里的線程、網(wǎng)絡(luò)連接池的連接,數(shù)據(jù)庫中數(shù)據(jù)引擎提供的鎖,等等一切可以被稱作競爭資源的東西。
1、線程池死鎖
用個(gè)例子來看看這個(gè)死鎖的特征:
final ExecutorService executorService =
Executors.newSingleThreadExecutor();
Future<Long> f1 = executorService.submit(new Callable<Long>() {
public Long call() throws Exception {
System.out.println("start f1");
Thread.sleep(1000);//延時(shí)
Future<Long> f2 =
executorService.submit(new Callable<Long>() {
public Long call() throws Exception {
System.out.println("start f2");
return -1L;
}
});
System.out.println("result" + f2.get());
System.out.println("end f1");
return -1L;
}
});
在這個(gè)例子中,線程池的任務(wù)1依賴任務(wù)2的執(zhí)行結(jié)果,但是線程池是單線程的,也就是說任務(wù)1不執(zhí)行完,任務(wù)2永遠(yuǎn)得不到執(zhí)行,那么因此造成了死鎖。原因圖解如下:

執(zhí)行jstack命令,可以看到如下內(nèi)容:
"pool-1-thread-1" prio=5 tid=0x00007ff4c10bf800 nid=0x3b03 waiting on condition [0x000000011628c000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000007ea51cf40> (a java.util.concurrent.FutureTask$Sync)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:186)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:834)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedInterruptibly(AbstractQueuedSynchronizer.java:994)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(AbstractQueuedSynchronizer.java:1303)
at java.util.concurrent.FutureTask$Sync.innerGet(FutureTask.java:248)
at java.util.concurrent.FutureTask.get(FutureTask.java:111)
at com.test.TestMain$1.call(TestMain.java:49)
at com.test.TestMain$1.call(TestMain.java:37)
at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:334)
at java.util.concurrent.FutureTask.run(FutureTask.java:166)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:722)
可以看到當(dāng)前線程wait在java.util.concurrent.FutureTask對(duì)象上。
解決辦法:擴(kuò)大線程池線程數(shù) or 任務(wù)結(jié)果之間不再互相依賴。
2、網(wǎng)絡(luò)連接池死鎖
同樣的,在網(wǎng)絡(luò)連接池也會(huì)發(fā)生死鎖,假設(shè)此時(shí)有兩個(gè)線程A和B,兩個(gè)數(shù)據(jù)庫連接池N1和N2,連接池大小都只有1,如果線程A按照先N1后N2的順序獲得網(wǎng)絡(luò)連接,而線程B按照先N2后N1的順序獲得網(wǎng)絡(luò)連接,并且兩個(gè)線程在完成執(zhí)行之前都不釋放自己已經(jīng)持有的鏈接,因此也造成了死鎖。
// 連接1
final MultiThreadedHttpConnectionManager connectionManager1 = new MultiThreadedHttpConnectionManager();
final HttpClient httpClient1 = new HttpClient(connectionManager1);
httpClient1.getHttpConnectionManager().getParams().setMaxTotalConnections(1); //設(shè)置整個(gè)連接池最大連接數(shù)
// 連接2
final MultiThreadedHttpConnectionManager connectionManager2 = new MultiThreadedHttpConnectionManager();
final HttpClient httpClient2 = new HttpClient(connectionManager2);
httpClient2.getHttpConnectionManager().getParams().setMaxTotalConnections(1); //設(shè)置整個(gè)連接池最大連接數(shù)
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.submit(new Runnable() {
public void run() {
try {
PostMethod httpost = new PostMethod("http://www.baidu.com");
System.out.println(">>>> Thread A execute 1 >>>>");
httpClient1.executeMethod(httpost);
Thread.sleep(5000l);
System.out.println(">>>> Thread A execute 2 >>>>");
httpClient2.executeMethod(httpost);
System.out.println(">>>> End Thread A>>>>");
} catch (Exception e) {
// ignore
}
}
});
executorService.submit(new Runnable() {
public void run() {
try {
PostMethod httpost = new PostMethod("http://www.baidu.com");
System.out.println(">>>> Thread B execute 2 >>>>");
httpClient2.executeMethod(httpost);
Thread.sleep(5000l);
System.out.println(">>>> Thread B execute 1 >>>>");
httpClient1.executeMethod(httpost);
System.out.println(">>>> End Thread B>>>>");
} catch (Exception e) {
// ignore
}
}
});
整個(gè)過程圖解如下:

在死鎖產(chǎn)生后,我們用jstack工具查看一下當(dāng)前線程堆棧信息,可以看到如下內(nèi)容:
"pool-1-thread-2" prio=5 tid=0x00007faa7909e800 nid=0x3b03 in Object.wait() [0x0000000111e5d000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000007ea73f498> (a org.apache.commons.httpclient.MultiThreadedHttpConnectionManager$ConnectionPool)
at org.apache.commons.httpclient.MultiThreadedHttpConnectionManager.doGetConnection(MultiThreadedHttpConnectionManager.java:518)
- locked <0x00000007ea73f498> (a org.apache.commons.httpclient.MultiThreadedHttpConnectionManager$ConnectionPool)
at org.apache.commons.httpclient.MultiThreadedHttpConnectionManager.getConnectionWithTimeout(MultiThreadedHttpConnectionManager.java:416)
at org.apache.commons.httpclient.HttpMethodDirector.executeMethod(HttpMethodDirector.java:153)
at org.apache.commons.httpclient.HttpClient.executeMethod(HttpClient.java:397)
at org.apache.commons.httpclient.HttpClient.executeMethod(HttpClient.java:323)
at com.test.TestMain$2.run(TestMain.java:79)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471)
at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:334)
at java.util.concurrent.FutureTask.run(FutureTask.java:166)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:722)
"pool-1-thread-1" prio=5 tid=0x00007faa7a039800 nid=0x3a03 in Object.wait() [0x0000000111d5a000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000007ea73e0d0> (a org.apache.commons.httpclient.MultiThreadedHttpConnectionManager$ConnectionPool)
at org.apache.commons.httpclient.MultiThreadedHttpConnectionManager.doGetConnection(MultiThreadedHttpConnectionManager.java:518)
- locked <0x00000007ea73e0d0> (a org.apache.commons.httpclient.MultiThreadedHttpConnectionManager$ConnectionPool)
at org.apache.commons.httpclient.MultiThreadedHttpConnectionManager.getConnectionWithTimeout(MultiThreadedHttpConnectionManager.java:416)
at org.apache.commons.httpclient.HttpMethodDirector.executeMethod(HttpMethodDirector.java:153)
at org.apache.commons.httpclient.HttpClient.executeMethod(HttpClient.java:397)
at org.apache.commons.httpclient.HttpClient.executeMethod(HttpClient.java:323)
at com.test.TestMain$1.run(TestMain.java:61)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471)
at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:334)
at java.util.concurrent.FutureTask.run(FutureTask.java:166)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:722)
當(dāng)然,我們?cè)谶@里只是一些極端情況的假定,假如線程在使用完連接池之后很快就歸還,在歸還連接數(shù)后才占用下一個(gè)連接池,那么死鎖也就不會(huì)發(fā)生。
總結(jié)
在我的理解當(dāng)中,死鎖就是“兩個(gè)任務(wù)以不合理的順序互相爭奪資源”造成,因此為了規(guī)避死鎖,應(yīng)用程序需要妥善處理資源獲取的順序。
另外有些時(shí)候,死鎖并不會(huì)馬上在應(yīng)用程序中體現(xiàn)出來,在通常情況下,都是應(yīng)用在生產(chǎn)環(huán)境運(yùn)行了一段時(shí)間后,才開始慢慢顯現(xiàn)出來,在實(shí)際測(cè)試過程中,由于死鎖的隱蔽性,很難在測(cè)試過程中及時(shí)發(fā)現(xiàn)死鎖的存在,而且在生產(chǎn)環(huán)境中,應(yīng)用出現(xiàn)了死鎖,往往都是在應(yīng)用狀況最糟糕的時(shí)候——在高負(fù)載情況下。因此,開發(fā)者在開發(fā)過程中要謹(jǐn)慎分析每個(gè)系統(tǒng)資源的使用情況,合理規(guī)避死鎖,另外一旦出現(xiàn)了死鎖,也可以嘗試使用本文中提到的一些工具,仔細(xì)分析,總是能找到問題所在的。
以上就是本次寫作全部內(nèi)容了,如果你喜歡,歡迎關(guān)注我的公眾號(hào)~
這是給我不斷寫作的最大鼓勵(lì),謝謝~
