Java面試必問-死鎖終極篇

江溢Jonny發表於2018-03-19

掘金江溢Jonny,轉載請註明原創出處,謝謝!

關注我的公眾號,獲得更多幹貨~

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確定當前執行任務的程式號:

jonny@~$ jps
597
1370 JConsole
1362 AppMain
1421 Jps
1361 Launcher
複製程式碼

可以確定任務程式號是1362,然後執行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自帶的監控工具,在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面試必問-死鎖終極篇

相關文章