Java面試必問-死鎖終極篇

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

關注個人公衆號,得到更多幹貨~ 算法

背景

這個話題是源自筆者之前跟人的一次技術討論,「你是怎麼發現死鎖的而且是如何預防、如何解決的?」之前聽到的這個問題的時候,雖然腦海裏也有一些思路,可是都是不夠系統化的東西。直到最近親身經歷一次死鎖,才作了這麼一次集中的思路整理,撰錄如下文字。但願對一樣問題的同窗有所幫助。數據庫

死鎖定義

首先咱們先來看看死鎖的定義:「死鎖是指兩個或兩個以上的進程在執行過程當中,因爲競爭資源或者因爲彼此通訊而形成的一種阻塞的現象,若無外力做用,它們都將沒法推動下去。」那麼咱們換一個更加規範的定義:「集合中的每個進程都在等待只能由本集合中的其餘進程才能引起的事件,那麼該組進程是死鎖的。」apache

競爭的資源能夠是:鎖、網絡鏈接、通知事件,磁盤、帶寬,以及一切能夠被稱做「資源」的東西。bash

舉個栗子

上面的內容可能有些抽象,所以咱們舉個例子來描述,若是此時有一個線程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();
}
複製代碼

程序執行結果以下: 工具

程序執行結果
很明顯,程序執行停滯了。

死鎖檢測

在這裏,我將介紹兩種死鎖檢測工具post

一、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對象

二、JConsole工具

Jconsole是JDK自帶的監控工具,在JDK/bin目錄下能夠找到。它用於鏈接正在運行的本地或者遠程的JVM,對運行在Java應用程序的資源消耗和性能進行監控,並畫出大量的圖表,提供強大的可視化界面。並且自己佔用的服務器內存很小,甚至能夠說幾乎不消耗。

咱們在命令行中敲入jconsole命令,會自動彈出如下對話框,選擇進程1362,並點擊「連接

新建鏈接

進入所檢測的進程後,選擇「線程」選項卡,並點擊「檢測死鎖」

檢測死鎖
能夠看到如下畫面:
死鎖檢測結果
能夠看到進程中存在死鎖。

以上例子我都是用synchronized關鍵詞實現的死鎖,若是讀者用ReentrantLock製造一次死鎖,再次使用死鎖檢測工具,也一樣能檢測到死鎖,不過顯示的信息將會更加豐富,有興趣的讀者能夠本身嘗試一下。

死鎖預防

若是一個線程每次只能得到一個鎖,那麼就不會產生鎖順序的死鎖。雖然不算很是現實,可是也很是正確(一個問題的最好解決辦法就是,這個問題剛好不會出現)。不過關於死鎖的預防,這裏有如下幾種方案:

一、以肯定的順序得到鎖

若是必須獲取多個鎖,那麼在設計的時候須要充分考慮不一樣線程以前得到鎖的順序。按照上面的例子,兩個線程得到鎖的時序圖以下:

時序圖

若是此時把得到鎖的時序改爲:

新時序圖
那麼死鎖就永遠不會發生。 針對兩個特定的鎖,開發者能夠嘗試按照鎖對象的hashCode值大小的順序,分別得到兩個鎖,這樣鎖老是會以特定的順序得到鎖,那麼死鎖也不會發生。
哲學家進餐

問題變得更加複雜一些,若是此時有多個線程,都在競爭不一樣的鎖,簡單按照鎖對象的hashCode進行排序(單純按照hashCode順序排序會出現「環路等待」),可能就沒法知足要求了,這個時候開發者可使用銀行家算法,全部的鎖都按照特定的順序獲取,一樣能夠防止死鎖的發生,該算法在這裏就再也不贅述了,有興趣的能夠自行了解一下。

二、超時放棄

當使用synchronized關鍵詞提供的內置鎖時,只要線程沒有得到鎖,那麼就會永遠等待下去,然而Lock接口提供了boolean tryLock(long time, TimeUnit unit) throws InterruptedException方法,該方法能夠按照固定時長等待鎖,所以線程能夠在獲取鎖超時之後,主動釋放以前已經得到的全部的鎖。經過這種方式,也能夠頗有效地避免死鎖。 仍是按照以前的例子,時序圖以下:

時序圖

其餘形式的死鎖

咱們再來回顧一下死鎖的定義,「死鎖是指兩個或兩個以上的進程在執行過程當中,因爲競爭資源或者因爲彼此通訊而形成的一種阻塞的現象,若無外力做用,它們都將沒法推動下去。」 死鎖條件裏面的競爭資源,能夠是線程池裏的線程、網絡鏈接池的鏈接,數據庫中數據引擎提供的鎖,等等一切能夠被稱做競爭資源的東西。

一、線程池死鎖

用個例子來看看這個死鎖的特徵:

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 任務結果之間再也不互相依賴。

二、網絡鏈接池死鎖

一樣的,在網絡鏈接池也會發生死鎖,假設此時有兩個線程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)
複製代碼

固然,咱們在這裏只是一些極端狀況的假定,假如線程在使用完鏈接池以後很快就歸還,在歸還鏈接數後才佔用下一個鏈接池,那麼死鎖也就不會發生。

總結

在個人理解當中,死鎖就是「兩個任務以不合理的順序互相爭奪資源」形成,所以爲了規避死鎖,應用程序須要妥善處理資源獲取的順序。 另外有些時候,死鎖並不會立刻在應用程序中體現出來,在一般狀況下,都是應用在生產環境運行了一段時間後,纔開始慢慢顯現出來,在實際測試過程當中,因爲死鎖的隱蔽性,很難在測試過程當中及時發現死鎖的存在,並且在生產環境中,應用出現了死鎖,每每都是在應用情況最糟糕的時候——在高負載狀況下。所以,開發者在開發過程當中要謹慎分析每一個系統資源的使用狀況,合理規避死鎖,另一旦出現了死鎖,也能夠嘗試使用本文中提到的一些工具,仔細分析,老是能找到問題所在的。

以上就是本次寫做所有內容了,若是你喜歡,歡迎關注個人公衆號~ 這是給我不斷寫做的最大鼓勵,謝謝~

相關文章
相關標籤/搜索