天天看點

Java面試必問-死鎖終極篇

背景

這個話題是源自筆者以前跟人的一次技術讨論,“你是怎麼發現死鎖的并且是如何預防、如何解決的?”以前聽到的這個問題的時候,雖然腦海裡也有一些思路,但是都是不夠系統化的東西。直到最近親身經曆一次死鎖,才做了這麼一次集中的思路整理,撰錄以下文字。希望對同樣問題的同學有所幫助。

死鎖定義

首先我們先來看看死鎖的定義:“死鎖是指兩個或兩個以上的程序在執行過程中,由于競争資源或者由于彼此通信而造成的一種阻塞的現象,若無外力作用,它們都将無法推進下去。”那麼我們換一個更加規範的定義:“集合中的每一個程序都在等待隻能由本集合中的其他程序才能引發的事件,那麼該組程序是死鎖的。”

競争的資源可以是:鎖、網絡連接配接、通知事件,磁盤、帶寬,以及一切可以被稱作“資源”的東西。

舉個栗子

上面的内容可能有些抽象,是以我們舉個例子來描述,如果此時有一個線程A,按照先鎖a再獲得鎖b的的順序獲得鎖,而在此同時又有另外一個線程B,按照先鎖b再鎖a的順序獲得鎖。如下圖所示:

Java面試必問-死鎖終極篇

死鎖

我們用一段代碼來模拟上述過程:

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();
                    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();
                    synchronized (a) {
                        System.out.println("now i in threadB-locka");
                    }
                } catch (Exception e) {
                    // ignore
                }
            }
        }
    });

    threadA.start();
    threadB.start();
}
                

程式執行結果如下:

Java面試必問-死鎖終極篇

程式執行結果

很明顯,程式執行停滞了。

死鎖檢測

在這裡,我将介紹兩種死鎖檢測工具

1、Jstack指令

jstack是java虛拟機自帶的一種堆棧跟蹤工具。jstack用于列印出給定的java程序ID或core file或遠端調試服務的Java堆棧資訊。

Jstack工具可以用于生成java虛拟機目前時刻的線程快照。線程快照是目前java虛拟機内每一條線程正在執行的方法堆棧的集合,生成線程快照的主要目的是定位線程出現長時間停頓的原因,如

線程間死鎖

死循環

請求外部資源導緻的長時間等待

等。 線程出現停頓的時候通過jstack來檢視各個線程的調用堆棧,就可以知道沒有響應的線程到底在背景做什麼事情,或者等待什麼資源。

首先,我們通過jps确定目前執行任務的程序号:

jonny@~$ jps

 JConsole
 AppMain
 Jps
 Launcher
                

可以确定任務程序号是1362,然後執行jstack指令檢視目前程序堆棧資訊:

jonny@~$ jstack -F 
Attaching to process ID , please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is -b01
Deadlock Detection:

Found one Java-level deadlock:
=============================

"Thread-1":
  waiting to lock Monitor@ (Object@, a java/lang/Object),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock Monitor@ (Object@, a java/lang/Object),
  which is held by "Thread-1"

Found a total of  deadlock.
                

可以看到,程序的确存在死鎖,兩個線程分别在等待對方持有的Object對象

2、JConsole工具

Jconsole是JDK自帶的監控工具,在JDK/bin目錄下可以找到。它用于連接配接正在運作的本地或者遠端的JVM,對運作在Java應用程式的資源消耗和性能進行監控,并畫出大量的圖表,提供強大的可視化界面。而且本身占用的伺服器記憶體很小,甚至可以說幾乎不消耗。

我們在指令行中敲入jconsole指令,會自動彈出以下對話框,選擇程序1362,并點選“連結”

Java面試必問-死鎖終極篇

建立連接配接

進入所檢測的程序後,選擇“線程”頁籤,并點選“檢測死鎖”

Java面試必問-死鎖終極篇

檢測死鎖

可以看到以下畫面:

Java面試必問-死鎖終極篇

死鎖檢測結果

可以看到程序中存在死鎖。

以上例子我都是用synchronized關鍵詞實作的死鎖,如果讀者用ReentrantLock制造一次死鎖,再次使用死鎖檢測工具,也同樣能檢測到死鎖,不過顯示的資訊将會更加豐富,有興趣的讀者可以自己嘗試一下。

死鎖預防

如果一個線程每次隻能獲得一個鎖,那麼就不會産生鎖順序的死鎖。雖然不算非常現實,但是也非常正确(一個問題的最好解決辦法就是,這個問題恰好不會出現)。不過關于死鎖的預防,這裡有以下幾種方案:

1、以确定的順序獲得鎖

如果必須擷取多個鎖,那麼在設計的時候需要充分考慮不同線程之前獲得鎖的順序。按照上面的例子,兩個線程獲得鎖的時序圖如下:

Java面試必問-死鎖終極篇

時序圖

如果此時把獲得鎖的時序改成:

Java面試必問-死鎖終極篇

新時序圖

那麼死鎖就永遠不會發生。

針對兩個特定的鎖,開發者可以嘗試按照鎖對象的hashCode值大小的順序,分别獲得兩個鎖,這樣鎖總是會以特定的順序獲得鎖,那麼死鎖也不會發生。

Java面試必問-死鎖終極篇

哲學家進餐

問題變得更加複雜一些,如果此時有多個線程,都在競争不同的鎖,簡單按照鎖對象的hashCode進行排序(單純按照hashCode順序排序會出現“環路等待”),可能就無法滿足要求了,這個時候開發者可以使用銀行家算法,所有的鎖都按照特定的順序擷取,同樣可以防止死鎖的發生,該算法在這裡就不再贅述了,有興趣的可以自行了解一下。

2、逾時放棄

當使用synchronized關鍵詞提供的内置鎖時,隻要線程沒有獲得鎖,那麼就會永遠等待下去,然而Lock接口提供了

boolean tryLock(long time, TimeUnit unit) throws InterruptedException

方法,該方法可以按照固定時長等待鎖,是以線程可以在擷取鎖逾時以後,主動釋放之前已經獲得的所有的鎖。通過這種方式,也可以很有效地避免死鎖。

還是按照之前的例子,時序圖如下:

Java面試必問-死鎖終極篇

時序圖

其他形式的死鎖

我們再來回顧一下死鎖的定義,“死鎖是指兩個或兩個以上的程序在執行過程中,由于競争資源或者由于彼此通信而造成的一種阻塞的現象,若無外力作用,它們都将無法推進下去。”

死鎖條件裡面的競争資源,可以是線程池裡的線程、網絡連接配接池的連接配接,資料庫中資料引擎提供的鎖,等等一切可以被稱作競争資源的東西。

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();//延時
        Future<Long> f2 = 
           executorService.submit(new Callable<Long>() {

            public Long call() throws Exception {
                System.out.println("start f2");
                return -;
            }
        });
        System.out.println("result" + f2.get());
        System.out.println("end f1");
        return -;
    }
});
                

在這個例子中,線程池的任務1依賴任務2的執行結果,但是線程池是單線程的,也就是說任務1不執行完,任務2永遠得不到執行,那麼是以造成了死鎖。原因圖解如下:

Java面試必問-死鎖終極篇

線程池死鎖

執行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();  //設定整個連接配接池最大連接配接數

// 連接配接2
final MultiThreadedHttpConnectionManager connectionManager2 = new MultiThreadedHttpConnectionManager();
final HttpClient httpClient2 = new HttpClient(connectionManager2);
httpClient2.getHttpConnectionManager().getParams().setMaxTotalConnections();  //設定整個連接配接池最大連接配接數

ExecutorService executorService = Executors.newFixedThreadPool();
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();

            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();

            System.out.println(">>>> Thread B execute 1 >>>>");
            httpClient1.executeMethod(httpost);
            System.out.println(">>>> End Thread B>>>>");

        } catch (Exception e) {
            // ignore
        }
    }
});
                

整個過程圖解如下:

Java面試必問-死鎖終極篇

連接配接池死鎖

在死鎖産生後,我們用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)
           

當然,我們在這裡隻是一些極端情況的假定,假如線程在使用完連接配接池之後很快就歸還,在歸還連接配接數後才占用下一個連接配接池,那麼死鎖也就不會發生。

總結

在我的了解當中,死鎖就是“兩個任務以不合理的順序互相争奪資源”造成,是以為了規避死鎖,應用程式需要妥善處理資源擷取的順序。

另外有些時候,死鎖并不會馬上在應用程式中展現出來,在通常情況下,都是應用在生産環境運作了一段時間後,才開始慢慢顯現出來,在實際測試過程中,由于死鎖的隐蔽性,很難在測試過程中及時發現死鎖的存在,而且在生産環境中,應用出現了死鎖,往往都是在應用狀況最糟糕的時候——在高負載情況下。是以,開發者在開發過程中要謹慎分析每個系統資源的使用情況,合理規避死鎖,另外一旦出現了死鎖,也可以嘗試使用本文中提到的一些工具,仔細分析,總是能找到問題所在的。