簡書江溢Jonny,轉(zhuǎn)載請注明原創(chuàng)出處攘残,謝謝序宦!
關注我的公眾號睁壁,獲得更多干貨~
背景
這個話題是源自筆者以前跟人的一次技術討論,“你是怎么發(fā)現(xiàn)死鎖的并且是如何預防互捌、如何解決的潘明?”以前聽到的這個問題的時候,雖然腦海里也有一些思路秕噪,但是都是不夠系統(tǒng)化的東西钳降。直到最近親身經(jīng)歷一次死鎖,才做了這么一次集中的思路整理腌巾,撰錄以下文字遂填。希望對同樣問題的同學有所幫助。
死鎖定義
首先我們先來看看死鎖的定義:“死鎖是指兩個或兩個以上的進程在執(zhí)行過程中澈蝙,由于競爭資源或者由于彼此通信而造成的一種阻塞的現(xiàn)象城菊,若無外力作用,它們都將無法推進下去碉克×杌#”那么我們換一個更加規(guī)范的定義:“集合中的每一個進程都在等待只能由本集合中的其他進程才能引發(fā)的事件,那么該組進程是死鎖的】退埃”
競爭的資源可以是:鎖况褪、網(wǎng)絡連接、通知事件更耻,磁盤测垛、帶寬,以及一切可以被稱作“資源”的東西秧均。
舉個栗子
上面的內(nèi)容可能有些抽象食侮,因此我們舉個例子來描述,如果此時有一個線程A目胡,按照先鎖a再獲得鎖b的的順序獲得鎖锯七,而在此同時又有另外一個線程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í)行停滯了。
死鎖檢測
在這里巨双,我將介紹兩種死鎖檢測工具
1噪猾、Jstack命令
jstack是java虛擬機自帶的一種堆棧跟蹤工具。jstack用于打印出給定的java進程ID或core file或遠程調(diào)試服務的Java堆棧信息筑累。
Jstack工具可以用于生成java虛擬機當前時刻的線程快照袱蜡。線程快照是當前java虛擬機內(nèi)每一條線程正在執(zhí)行的方法堆棧的集合,生成線程快照的主要目的是定位線程出現(xiàn)長時間停頓的原因慢宗,如線程間死鎖
戒劫、死循環(huán)
、請求外部資源導致的長時間等待
等婆廊。 線程出現(xiàn)停頓的時候通過jstack來查看各個線程的調(diào)用堆棧迅细,就可以知道沒有響應的線程到底在后臺做什么事情,或者等待什么資源淘邻。
首先茵典,我們通過jps確定當前執(zhí)行任務的進程號:
jonny@~$ jps
597
1370 JConsole
1362 AppMain
1421 Jps
1361 Launcher
可以確定任務進程號是1362,然后執(zhí)行jstack命令查看當前進程堆棧信息:
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.
可以看到宾舅,進程的確存在死鎖统阿,兩個線程分別在等待對方持有的Object對象
2、JConsole工具
Jconsole是JDK自帶的監(jiān)控工具筹我,在JDK/bin目錄下可以找到扶平。它用于連接正在運行的本地或者遠程的JVM,對運行在Java應用程序的資源消耗和性能進行監(jiān)控蔬蕊,并畫出大量的圖表结澄,提供強大的可視化界面。而且本身占用的服務器內(nèi)存很小,甚至可以說幾乎不消耗麻献。
我們在命令行中敲入jconsole命令们妥,會自動彈出以下對話框,選擇進程1362勉吻,并點擊“鏈接”
進入所檢測的進程后监婶,選擇“線程”選項卡,并點擊“檢測死鎖”
可以看到以下畫面:
可以看到進程中存在死鎖齿桃。
以上例子我都是用synchronized關鍵詞實現(xiàn)的死鎖惑惶,如果讀者用ReentrantLock制造一次死鎖,再次使用死鎖檢測工具短纵,也同樣能檢測到死鎖带污,不過顯示的信息將會更加豐富,有興趣的讀者可以自己嘗試一下踩娘。
死鎖預防
如果一個線程每次只能獲得一個鎖,那么就不會產(chǎn)生鎖順序的死鎖喉祭。雖然不算非逞剩現(xiàn)實,但是也非常正確(一個問題的最好解決辦法就是泛烙,這個問題恰好不會出現(xiàn))理卑。不過關于死鎖的預防,這里有以下幾種方案:
1蔽氨、以確定的順序獲得鎖
如果必須獲取多個鎖藐唠,那么在設計的時候需要充分考慮不同線程之前獲得鎖的順序。按照上面的例子鹉究,兩個線程獲得鎖的時序圖如下:
如果此時把獲得鎖的時序改成:
那么死鎖就永遠不會發(fā)生宇立。
針對兩個特定的鎖,開發(fā)者可以嘗試按照鎖對象的hashCode值大小的順序自赔,分別獲得兩個鎖妈嘹,這樣鎖總是會以特定的順序獲得鎖,那么死鎖也不會發(fā)生绍妨。
問題變得更加復雜一些润脸,如果此時有多個線程,都在競爭不同的鎖他去,簡單按照鎖對象的hashCode進行排序(單純按照hashCode順序排序會出現(xiàn)“環(huán)路等待”)毙驯,可能就無法滿足要求了,這個時候開發(fā)者可以使用銀行家算法灾测,所有的鎖都按照特定的順序獲取爆价,同樣可以防止死鎖的發(fā)生,該算法在這里就不再贅述了,有興趣的可以自行了解一下允坚。
2魂那、超時放棄
當使用synchronized關鍵詞提供的內(nèi)置鎖時,只要線程沒有獲得鎖稠项,那么就會永遠等待下去涯雅,然而Lock接口提供了boolean tryLock(long time, TimeUnit unit) throws InterruptedException
方法,該方法可以按照固定時長等待鎖展运,因此線程可以在獲取鎖超時以后活逆,主動釋放之前已經(jīng)獲得的所有的鎖。通過這種方式拗胜,也可以很有效地避免死鎖蔗候。
還是按照之前的例子,時序圖如下:
其他形式的死鎖
我們再來回顧一下死鎖的定義埂软,“死鎖是指兩個或兩個以上的進程在執(zhí)行過程中锈遥,由于競爭資源或者由于彼此通信而造成的一種阻塞的現(xiàn)象,若無外力作用勘畔,它們都將無法推進下去所灸。”
死鎖條件里面的競爭資源炫七,可以是線程池里的線程爬立、網(wǎng)絡連接池的連接,數(shù)據(jù)庫中數(shù)據(jù)引擎提供的鎖万哪,等等一切可以被稱作競爭資源的東西侠驯。
1、線程池死鎖
用個例子來看看這個死鎖的特征:
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);//延時
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;
}
});
在這個例子中奕巍,線程池的任務1依賴任務2的執(zhí)行結(jié)果吟策,但是線程池是單線程的,也就是說任務1不執(zhí)行完的止,任務2永遠得不到執(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)
可以看到當前線程wait在java.util.concurrent.FutureTask對象上效床。
解決辦法:擴大線程池線程數(shù) or 任務結(jié)果之間不再互相依賴。
2权谁、網(wǎng)絡連接池死鎖
同樣的剩檀,在網(wǎng)絡連接池也會發(fā)生死鎖,假設此時有兩個線程A和B旺芽,兩個數(shù)據(jù)庫連接池N1和N2沪猴,連接池大小都只有1辐啄,如果線程A按照先N1后N2的順序獲得網(wǎng)絡連接,而線程B按照先N2后N1的順序獲得網(wǎng)絡連接运嗜,并且兩個線程在完成執(zhí)行之前都不釋放自己已經(jīng)持有的鏈接壶辜,因此也造成了死鎖。
// 連接1
final MultiThreadedHttpConnectionManager connectionManager1 = new MultiThreadedHttpConnectionManager();
final HttpClient httpClient1 = new HttpClient(connectionManager1);
httpClient1.getHttpConnectionManager().getParams().setMaxTotalConnections(1); //設置整個連接池最大連接數(shù)
// 連接2
final MultiThreadedHttpConnectionManager connectionManager2 = new MultiThreadedHttpConnectionManager();
final HttpClient httpClient2 = new HttpClient(connectionManager2);
httpClient2.getHttpConnectionManager().getParams().setMaxTotalConnections(1); //設置整個連接池最大連接數(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
}
}
});
整個過程圖解如下:
在死鎖產(chǎn)生后担租,我們用jstack工具查看一下當前線程堆棧信息砸民,可以看到如下內(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)
當然,我們在這里只是一些極端情況的假定奋救,假如線程在使用完連接池之后很快就歸還岭参,在歸還連接數(shù)后才占用下一個連接池,那么死鎖也就不會發(fā)生尝艘。
總結(jié)
在我的理解當中演侯,死鎖就是“兩個任務以不合理的順序互相爭奪資源”造成,因此為了規(guī)避死鎖背亥,應用程序需要妥善處理資源獲取的順序秒际。
另外有些時候,死鎖并不會馬上在應用程序中體現(xiàn)出來狡汉,在通常情況下娄徊,都是應用在生產(chǎn)環(huán)境運行了一段時間后,才開始慢慢顯現(xiàn)出來轴猎,在實際測試過程中嵌莉,由于死鎖的隱蔽性进萄,很難在測試過程中及時發(fā)現(xiàn)死鎖的存在捻脖,而且在生產(chǎn)環(huán)境中,應用出現(xiàn)了死鎖中鼠,往往都是在應用狀況最糟糕的時候——在高負載情況下可婶。因此,開發(fā)者在開發(fā)過程中要謹慎分析每個系統(tǒng)資源的使用情況援雇,合理規(guī)避死鎖矛渴,另外一旦出現(xiàn)了死鎖,也可以嘗試使用本文中提到的一些工具惫搏,仔細分析具温,總是能找到問題所在的。
以上就是本次寫作全部內(nèi)容了筐赔,如果你喜歡铣猩,歡迎關注我的公眾號~
這是給我不斷寫作的最大鼓勵,謝謝~