Java多線程-死鎖經(jīng)驗談

簡書江溢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í)行結(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勉吻,并點擊“鏈接

新建連接

進入所檢測的進程后监婶,選擇“線程”選項卡,并點擊“檢測死鎖”


檢測死鎖

可以看到以下畫面:


死鎖檢測結(jié)果

可以看到進程中存在死鎖齿桃。

以上例子我都是用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)容了筐赔,如果你喜歡铣猩,歡迎關注我的公眾號~
這是給我不斷寫作的最大鼓勵,謝謝~


最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末茴丰,一起剝皮案震驚了整個濱河市达皿,隨后出現(xiàn)的幾起案子天吓,更是在濱河造成了極大的恐慌,老刑警劉巖峦椰,帶你破解...
    沈念sama閱讀 221,820評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件龄寞,死亡現(xiàn)場離奇詭異,居然都是意外死亡汤功,警方通過查閱死者的電腦和手機物邑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來冤竹,“玉大人拂封,你說我怎么就攤上這事○腥洌” “怎么了冒签?”我有些...
    開封第一講書人閱讀 168,324評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長钟病。 經(jīng)常有香客問我萧恕,道長,這世上最難降的妖魔是什么肠阱? 我笑而不...
    開封第一講書人閱讀 59,714評論 1 297
  • 正文 為了忘掉前任票唆,我火速辦了婚禮,結(jié)果婚禮上屹徘,老公的妹妹穿的比我還像新娘走趋。我一直安慰自己,他們只是感情好噪伊,可當我...
    茶點故事閱讀 68,724評論 6 397
  • 文/花漫 我一把揭開白布簿煌。 她就那樣靜靜地躺著,像睡著了一般鉴吹。 火紅的嫁衣襯著肌膚如雪姨伟。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,328評論 1 310
  • 那天豆励,我揣著相機與錄音夺荒,去河邊找鬼。 笑死良蒸,一個胖子當著我的面吹牛技扼,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播嫩痰,決...
    沈念sama閱讀 40,897評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼剿吻,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了始赎?” 一聲冷哼從身側(cè)響起和橙,我...
    開封第一講書人閱讀 39,804評論 0 276
  • 序言:老撾萬榮一對情侶失蹤仔燕,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后魔招,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體晰搀,經(jīng)...
    沈念sama閱讀 46,345評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,431評論 3 340
  • 正文 我和宋清朗相戀三年办斑,在試婚紗的時候發(fā)現(xiàn)自己被綠了外恕。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,561評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡乡翅,死狀恐怖鳞疲,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蠕蚜,我是刑警寧澤尚洽,帶...
    沈念sama閱讀 36,238評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站靶累,受9級特大地震影響腺毫,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜挣柬,卻給世界環(huán)境...
    茶點故事閱讀 41,928評論 3 334
  • 文/蒙蒙 一潮酒、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧邪蛔,春花似錦急黎、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至床牧,卻和暖如春荣回,著一層夾襖步出監(jiān)牢的瞬間遭贸,已是汗流浹背戈咳。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留壕吹,地道東北人著蛙。 一個月前我還...
    沈念sama閱讀 48,983評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像耳贬,于是被迫代替她去往敵國和親踏堡。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,573評論 2 359

推薦閱讀更多精彩內(nèi)容