Java面試必問-死鎖終極篇
背景
這個話題是源自筆者以前跟人的一次技術討論,“你是怎麼發現死鎖的並且是如何預防、如何解決的?”以前聽到的這個問題的時候,雖然腦海裡也有一些思路,但是都是不夠系統化的東西。直到最近親身經歷一次死鎖,才做了這麼一次集中的思路整理,撰錄以下文字。希望對同樣問題的同學有所幫助。
死鎖定義
首先我們先來看看死鎖的定義:“死鎖是指兩個或兩個以上的程序在執行過程中,由於競爭資源或者由於彼此通訊而造成的一種阻塞的現象,若無外力作用,它們都將無法推進下去。”那麼我們換一個更加規範的定義:“集合中的每一個程序都在等待只能由本集合中的其他程序才能引發的事件,那麼該組程序是死鎖的。”
競爭的資源可以是:鎖、網路連線、通知事件,磁碟、頻寬,以及一切可以被稱作“資源”的東西。
舉個栗子
上面的內容可能有些抽象,因此我們舉個例子來描述,如果此時有一個執行緒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();
}
程式執行結果如下:
程式執行結果
很明顯,程式執行停滯了。
死鎖檢測
在這裡,我將介紹兩種死鎖檢測工具
1、Jstack命令
jstack是java虛擬機器自帶的一種堆疊跟蹤工具。jstack用於打印出給定的java程序ID或core file或遠端除錯服務的Java堆疊資訊。
Jstack工具可以用於生成java虛擬機器當前時刻的執行緒快照。執行緒快照是當前java虛擬機器內每一條執行緒正在執行的方法堆疊的集合,生成執行緒快照的主要目的是定位執行緒出現長時間停頓的原因,如執行緒間死鎖
、死迴圈
、請求外部資源導致的長時間等待
等。 執行緒出現停頓的時候通過jstack來檢視各個執行緒的呼叫堆疊,就可以知道沒有響應的執行緒到底在後臺做什麼事情,或者等待什麼資源。
首先,我們通過jps確定當前執行任務的程序號:
[email protected]~$ jps
597
1370 JConsole
1362 AppMain
1421 Jps
1361 Launcher
可以確定任務程序號是1362,然後執行jstack命令檢視當前程序堆疊資訊:
[email protected]~$ 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 [email protected]0x00007fea1900f6b8 (Object@0x00000007efa684c8, a java/lang/Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock [email protected]0x00007fea1900ceb0 (Object@0x00000007efa684d8, a java/lang/Object),
which is held by "Thread-1"
Found a total of 1 deadlock.
可以看到,程序的確存在死鎖,兩個執行緒分別在等待對方持有的Object物件
2、JConsole工具
Jconsole是JDK自帶的監控工具,在JDK/bin目錄下可以找到。它用於連線正在執行的本地或者遠端的JVM,對執行在Java應用程式的資源消耗和效能進行監控,並畫出大量的圖表,提供強大的視覺化介面。而且本身佔用的伺服器記憶體很小,甚至可以說幾乎不消耗。
我們在命令列中敲入jconsole命令,會自動彈出以下對話方塊,選擇程序1362,並點選“連結”
新建連線進入所檢測的程序後,選擇“執行緒”選項卡,並點選“檢測死鎖”
檢測死鎖
可以看到以下畫面:
死鎖檢測結果
可以看到程序中存在死鎖。
以上例子我都是用synchronized關鍵詞實現的死鎖,如果讀者用ReentrantLock製造一次死鎖,再次使用死鎖檢測工具,也同樣能檢測到死鎖,不過顯示的資訊將會更加豐富,有興趣的讀者可以自己嘗試一下。
死鎖預防
如果一個執行緒每次只能獲得一個鎖,那麼就不會產生鎖順序的死鎖。雖然不算非常現實,但是也非常正確(一個問題的最好解決辦法就是,這個問題恰好不會出現)。不過關於死鎖的預防,這裡有以下幾種方案:
1、以確定的順序獲得鎖
如果必須獲取多個鎖,那麼在設計的時候需要充分考慮不同執行緒之前獲得鎖的順序。按照上面的例子,兩個執行緒獲得鎖的時序圖如下:
時序圖
如果此時把獲得鎖的時序改成:
新時序圖
那麼死鎖就永遠不會發生。
針對兩個特定的鎖,開發者可以嘗試按照鎖物件的hashCode值大小的順序,分別獲得兩個鎖,這樣鎖總是會以特定的順序獲得鎖,那麼死鎖也不會發生。
哲學家進餐
問題變得更加複雜一些,如果此時有多個執行緒,都在競爭不同的鎖,簡單按照鎖物件的hashCode進行排序(單純按照hashCode順序排序會出現“環路等待”),可能就無法滿足要求了,這個時候開發者可以使用銀行家演算法,所有的鎖都按照特定的順序獲取,同樣可以防止死鎖的發生,該演算法在這裡就不再贅述了,有興趣的可以自行了解一下。
2、超時放棄
當使用synchronized關鍵詞提供的內建鎖時,只要執行緒沒有獲得鎖,那麼就會永遠等待下去,然而Lock介面提供了boolean tryLock(long time, TimeUnit unit) throws InterruptedException
方法,該方法可以按照固定時長等待鎖,因此執行緒可以在獲取鎖超時以後,主動釋放之前已經獲得的所有的鎖。通過這種方式,也可以很有效地避免死鎖。
還是按照之前的例子,時序圖如下:
其他形式的死鎖
我們再來回顧一下死鎖的定義,“死鎖是指兩個或兩個以上的程序在執行過程中,由於競爭資源或者由於彼此通訊而造成的一種阻塞的現象,若無外力作用,它們都將無法推進下去。”
死鎖條件裡面的競爭資源,可以是執行緒池裡的執行緒、網路連線池的連線,資料庫中資料引擎提供的鎖,等等一切可以被稱作競爭資源的東西。
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的執行結果,但是執行緒池是單執行緒的,也就是說任務1不執行完,任務2永遠得不到執行,那麼因此造成了死鎖。原因圖解如下:
執行緒池死鎖
執行jstack命令,可以看到如下內容:
"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物件上。
解決辦法:擴大執行緒池執行緒數 or 任務結果之間不再互相依賴。
2、網路連線池死鎖
同樣的,在網路連線池也會發生死鎖,假設此時有兩個執行緒A和B,兩個資料庫連線池N1和N2,連線池大小都只有1,如果執行緒A按照先N1後N2的順序獲得網路連線,而執行緒B按照先N2後N1的順序獲得網路連線,並且兩個執行緒在完成執行之前都不釋放自己已經持有的連結,因此也造成了死鎖。
// 連線1
final MultiThreadedHttpConnectionManager connectionManager1 = new MultiThreadedHttpConnectionManager();
final HttpClient httpClient1 = new HttpClient(connectionManager1);
httpClient1.getHttpConnectionManager().getParams().setMaxTotalConnections(1); //設定整個連線池最大連線數
// 連線2
final MultiThreadedHttpConnectionManager connectionManager2 = new MultiThreadedHttpConnectionManager();
final HttpClient httpClient2 = new HttpClient(connectionManager2);
httpClient2.getHttpConnectionManager().getParams().setMaxTotalConnections(1); //設定整個連線池最大連線數
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
}
}
});
整個過程圖解如下:
連線池死鎖
在死鎖產生後,我們用jstack工具檢視一下當前執行緒堆疊資訊,可以看到如下內容:
"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)
當然,我們在這裡只是一些極端情況的假定,假如執行緒在使用完連線池之後很快就歸還,在歸還連線數後才佔用下一個連線池,那麼死鎖也就不會發生。
總結
在我的理解當中,死鎖就是“兩個任務以不合理的順序互相爭奪資源”造成,因此為了規避死鎖,應用程式需要妥善處理資源獲取的順序。
另外有些時候,死鎖並不會馬上在應用程式中體現出來,在通常情況下,都是應用在生產環境運行了一段時間後,才開始慢慢顯現出來,在實際測試過程中,由於死鎖的隱蔽性,很難在測試過程中及時發現死鎖的存在,而且在生產環境中,應用出現了死鎖,往往都是在應用狀況最糟糕的時候——在高負載情況下。因此,開發者在開發過程中要謹慎分析每個系統資源的使用情況,合理規避死鎖,另外一旦出現了死鎖,也可以嘗試使用本文中提到的一些工具,仔細分析,總是能找到問題所在的。
相關推薦
Java面試必問-死鎖終極篇
背景這個話題是源自筆者以前跟人的一次技術討論,“你是怎麼發現死鎖的並且是如何預防、如何解決的?”以前聽到的這個問題的時候,雖然腦海裡也有一些思路,但是都是不夠系統化的東西。直到最近親身經歷一次死鎖,才做了這麼一次集中的思路整理,撰錄以下文字。希望對同樣問題的同學有所幫助。死鎖定義首先我們先來看看死鎖的定義:“
Java面試必問:ThreadLocal終極篇 淦!
點贊再看,養成習慣,微信搜一搜【敖丙】關注這個網際網路苟且偷生的程式設計師。 本文 GitHub https://github.com/JavaFamily 已收錄,有一線大廠面試完整考點、資料以及我的系列文章。 開場白 張三最近天氣很熱心情不是很好,所以他決定出去面試跟面試官聊聊天排解一下,結果剛投遞簡
java面試必問——六大排序演算法
java中有很多中排序方法,其中氣泡排序過於簡單,基數排序主要用於研究我們這裡不討論。實際應用和麵試中,最常問到的就是下面的六種排序方法,我們將從原理,複雜度,穩定性和實際應用幾個方面來討論他們。 選擇排序 排序原理: 選擇排序從整個待排序陣列N中
java面試必問面試題
一、八種基本資料型別 (1)boolean:只有true和false兩個取值。 (2)byte:8位,最大儲存資料量是255,存放的資料範圍是-128~127之間。 (3)short:16位,最大資料儲存量是65536,資料範圍是-32768~32767之
Java面試必問
code ava int 什麽 hashmap ash val 不可 intval 1. 圖靈 1.1 聊聊哈希算法與HashMap 1)一個優秀的哈希算法有什麽特點? 快速、不可逆、敏感性、低碰撞性 2)自己寫一個Hash算法 取模 3)Java中的Hash
Java面試必問之Hashmap底層實現原理(JDK1.7)
# 1. 前言 Hashmap可以說是Java面試必問的,一般的面試題會問: * Hashmap有哪些特性? * Hashmap底層實現原理(get\put\resize) * Hashmap怎麼解決hash衝突? * Hashmap是執行緒安全的嗎? * ... 今天就從原始碼角度一探究竟。筆者的原始
Java面試必問之Hashmap底層實現原理(JDK1.8)
# 1. 前言 上一篇從原始碼方面瞭解了JDK1.7中Hashmap的實現原理,可以看到其原始碼相對還是比較簡單的。本篇筆者和大家一起學習下JDK1.8下Hashmap的實現。JDK1.8中對Hashmap做了以下改動。 - 預設初始化容量=0 - 引入紅黑樹,優化資料結構 - 將連結串列頭插法改為尾插法
"Java基礎"-Java,Android面試必問部分
關於文章內容: 大家好,今天我打算整理並總結關於JAVA,Android的相關方面的技能點,主要分為: 1.java基礎板塊; 3.andoroid基礎板塊; 4.android高階板塊. 如果大家認真掌握好,那麼你就相當於有了兩年以上的開發經
Java高階面試必問—Dubbo面試題彙總
1、預設使用的是什麼通訊框架,還有別的選擇嗎? 2、服務呼叫是阻塞的嗎? 3、一般使用什麼註冊中心?還有別的選擇嗎? 4、預設使用什麼序列化框架,你知道的還有哪些? 5、服務提供者能實現失效踢出是什麼原理? 6、服務上線怎麼不影響舊版本?
Java基礎面試必問
1.&與&&區別?&和&&都是邏輯運算子,都是判斷兩邊同時真則為真,否則為假;但是&&當第一個條件不成之後,後面的條件都不執行了,而&am
"工廠模式"-之Java,Android面試必問設計模式(1/9)
1.工廠方法模式:分三種 這是我給大家介紹的8個面試必問的設計模式的第一類:工廠模式.學完這8類必會的那麼面試就不會有什麼問題了,其餘的無非都是多型的演變.好的開始講解第一類: 1.普通工
"裝飾模式"-之Java,Android面試必問設計模式(6/9)
下面我們開始講到面試必問的第六種設計模式:裝飾模式 “裝飾模式** 概念: 為了給一個物件增加功能:一個介面,一個具體類實現了方法,為了拓展方法,增加一個”裝飾類”繼承介面,(裝飾類裡宣告
"單例模式"-之Java,Android面試必問設計模式(3/9)
單例模式 下面給大家分享面試必問8大設計模式中的第三種:單例模式 大家如果被面試官問到單例模式,只需回答下面的內容,並分析其中的幾個關鍵點即可. 單例模式分兩類: 1.開發使用餓漢式 2.但是懶漢式是必須會寫的 要求解釋未加鎖版的缺陷,和解決
面試必問的MySQL鎖與事務隔離級別
之前多篇文章從mysql的底層結構分析、sql語句的分析器以及sql從優化底層分析, 還有工作中常用的sql優化小知識點。面試各大網際網路公司必問的mysql鎖和事務隔離級別,這篇文章給你打神助攻,一飛沖天。 鎖定義 鎖是計算機協調多個程序或執行緒併發訪問某一資源的機制。 在資料庫中,除了傳統的計算資源(
面試必問:分散式鎖實現之zk(Zookeeper)
點贊再看,養成習慣,微信搜尋【三太子敖丙】關注這個網際網路苟且偷生的工具人。 本文 GitHub https://github.com/JavaFamily 已收錄,有一線大廠面試完整考點、資料以及我的系列文章。 前言 鎖我想不需要我過多的去說,大家都知道是怎麼一回事了吧? 在多執行緒環境下,由於
java面試常問問題及部分答案(2018)
java 面試 2018年 基礎 一:java基礎1.簡述string對象,StringBuffer、StringBuilder區分string是final的,內部用一個final類型的char數組存儲數據,它的拼接效率比較低,實際上是通過建立一個StringBuffer,讓後臺調用appen
面試必問之JVM原理
清理 返回結果 機器碼 抽象 手機 最長 () 參數配置 包括 1:什麽是JVM JVM是Java Virtual Machine(Java虛擬機)的縮寫,JVM是一種用於計算設備的規範,它是一個虛構出來的計算機,是通過在實際的計算機上仿真模擬各種計算機功能來實現的。Jav
【面試必問】python實例方法、類方法@classmethod、靜態方法@staticmethod和屬性方法@property區別
區別 實例 實例變量 對象 s參數 pro 當前 靜態方法 實例方法 【面試必問】python實例方法、類方法@classmethod、靜態方法@staticmethod和屬性方法@property區別 1、#類方法@classmethod,只能訪問類變量,不能訪問實例變量
(轉)ThreadLocal-面試必問深度解析
ThreadLocal是什麼 ThreadLocal是一個本地執行緒副本變數工具類。主要用於將私有執行緒和該執行緒存放的副本物件做一個對映,各個執行緒之間的變數互不干擾,在高併發場景下,可以實現無狀態的呼叫,特別適用於各個執行緒依賴不通的變數值完成操作的場景。 從資料結構入手
Java面試通關要點【問題彙總篇】
基礎篇 基本功: 面向物件的特徵 final, finally, finalize 的區別 int 和 Integer 有什麼區別 過載和重寫的區別 抽象類和介面有什麼區別 說說反射的用途及實現 說說自定義註解的場景及實現 HTTP 請求的 GET 與 POST 方式的區