## 14 僵持不下—死鎖詳解
> 天才就是這樣,終身努力,便成天才。
> ——門捷列夫
前面幾節講解了并發的三大特性 - 原子性、可見性、有序性。解決這些問題的關鍵就是同步,而一種重要的同步方式就是加鎖,所謂的加鎖就是某個線程聲明某個資源暫時由我獨享,等其用完后,此線程解鎖,也就是放棄對該資源的占有。如果有其它線程等待使用該資源,那么會再次對此資源加鎖。所謂的死鎖,其實就是因為某種原因,達不到解鎖的條件,導致某線程對資源的占有無法釋放,其他線程會一直等待其解鎖,而被一直 block 住。
## 1\. 死鎖產生原因
產生死鎖的原因很多,我們逐個來看一下:
1. 交叉死鎖
A 線程持有資源 R1 的鎖時,想要獲取 R2 的鎖。而線程 B 此時持有 R2 的鎖,想要獲取 R1 的鎖。結果就是兩個線程互相等待對方釋放,并且一直等待下去。這就像兩個小孩打架摟抱在一起,A 說:你放手!B 說:你先放手!A 說:你先放手我就放手!B 說:憑什么我先放手,你先放手我就放手!瞧,是不是死鎖了,最后估計還得繼續打下去。

2. 內存不足
某系統內存 20M,兩個線程正在分別執行任務,各自已經使用了 10M 內存。但是執行到一半時需要更大的內存,但是系統已經沒有內存可供使用。那么兩個線程都會等待對方執行完畢 時釋放內存。這就造成了兩個線程互相等待,從而形成死鎖。
3. 一問一答式的數據交換
所謂的一問一答式數據交換就是客戶端發送請求,服務端返回響應。如果在交互過程中出現了數據的丟失,雙方產生誤解,以為對方沒有收到消息,陷入等待之中。如果此時沒有設置 timeout,就會造成互相的等待一直持續下去,從而形成死鎖。
4. 數據庫鎖
如果某個線程對數據庫表或者行加鎖,但是意外導致沒能正確釋放鎖,而其他線程則會等待數據庫鎖的釋放,從而陷入死鎖。
5. 文件鎖
某個線程獲取文件鎖后開始執行。但是執行過程中意外退出,而沒能釋放鎖。那么其他等待該文件鎖的線程將會一直等待,直到系統釋放文件句柄的資源。
6. 死循環
假如某個線程,由于編碼問題,在對資源加鎖后,陷入死循環,導致一致無法釋放鎖。
## 2 . 死鎖舉例
下面我們看一個交叉死鎖的例子,來切身感受下死鎖是如何煉成的。例子很簡單,DeadLock 類有一個讀方法和一個寫方法,讀方法獲取讀鎖后,又嘗試獲取寫鎖。而寫方法獲取寫鎖后,又嘗試獲取讀鎖。這種情況下,兩個線程會互相等待對方的鎖釋放,從而形成了死鎖。我們看下面的代碼:
~~~java
public class DeadLock {
private final String write_lock = new String();
private final String read_lock = new String();
public void read() {
synchronized (read_lock) {
System.out.println(Thread.currentThread().getName() + " got read lock and then i want to write");
synchronized (write_lock) {
System.out.println(Thread.currentThread().getName() + " got read lock and write lock");
}
}
}
public void write() {
synchronized (write_lock) {
System.out.println(Thread.currentThread().getName() + " got write lock and then i want to read");
synchronized (read_lock) {
System.out.println(Thread.currentThread().getName() + " got write lock and read lock");
}
}
}
public static void main(String[] args) {
DeadLock deadLock = new DeadLock();
new Thread(() -> {
while (true) {
deadLock.read();
}
},"read-first-thread").start();
new Thread(() -> {
while (true) {
deadLock.write();
}
},"write-first-thread").start();
}
}
~~~
注意 mian 方法中使用了 lambda 表達式為 thread 提供了 run 方法的現實。免去了我們編寫兩個實現 run 方法類的麻煩。這段程序運行后,控制臺輸出如下:
~~~
read-first-thread got read lock and then i want to write
read-first-thread got read lock and write lock
read-first-thread got read lock and then i want to write
read-first-thread got read lock and write lock
read-first-thread got read lock and then i want to write
read-first-thread got read lock and write lock
read-first-thread got read lock and then i want to write
write-first-thread got write lock and then i want to read
~~~
可以看到在 write 線程啟動前,一切正常。read-first-thread 線程能夠先后獲得 read 鎖和 write 鎖。但是當 write 線程啟動后,立刻出現了問題,日志不再打印,而是停留在 write 線程等待 read 鎖這一步。這是因為已經死鎖了。 read 線程在等 write 線程釋放寫鎖,而 write 線程在等 read 線程釋放讀鎖。兩個線程就會如此一直等下去了。
## 3\. 死鎖診斷
線程死鎖可以通過 java 的監控工具來查看。此類工具很多,例如 jstack、jconsole、jprofile 等。下面我們看一下 Java 內置的 jconsole。如果你安裝了 JDK,設置好了環境變量,那么可以直接在控制臺輸入 jconsole 來運行,界面如下:

在本地進程中我們可以看到剛才運行的 DeadLock。選中后點擊連接。在下一個界面上面的菜單選擇線程。在下方左側框中可以看到 write 和 read 兩個線程。然后我們點擊檢查死鎖,顯示如下圖:

點擊某個線程,如 write-first-thread,右側框中出現此線程的狀態,可以看到狀態為 java.lang.String@1adb219c 上的 BLOCKED,擁有者: read-first-thread。意思是此線程在 java.lang.String@1adb219c 上被 block 住了,這個 String 對象其實就是 read\_lock 對象,目前鎖的擁有者是 read-first-thread。我們再查看另外一個線程 read-first-thread,可以看到正好是反過來的。這兩個線程互相 block 住了。

## 4\. 總結
本節中我們列舉了多種引起死鎖的原因,這對我們分析死鎖的產生很有幫助,也有助于我們從代碼層面找到可能導致死鎖的風險。后面通過舉例,更為形象的切身感受到死鎖的產生過程。即使我們知道死鎖產生的原理,但也還是很可能寫出導致死鎖的代碼,那么出現死鎖或者疑似死鎖的時候應該怎么辦呢?最后也給出了答案。
至此本章已經結束。本章主要講解了多線程程序中會遇到的問題。并且針對如何解決這些問題,做了一些簡單的講解。在接下來的一章中,我們將會深入學習 Java 為我們提供的解決這些問題的工具。我們不但在遇到問題的時候要知道如何解決,還應該了解解決問題的原理是什么。
- 前言
- 第1章 Java并發簡介
- 01 開篇詞:多線程為什么是你必需要掌握的知識
- 02 絕對不僅僅是為了面試—我們為什么需要學習多線程
- 03 多線程開發如此簡單—Java中如何編寫多線程程序
- 04 人多力量未必大—并發可能會遇到的問題
- 第2章 Java中如何編寫多線程
- 05 看若兄弟,實如父子—Thread和Runnable詳解
- 06 線程什么時候開始真正執行?—線程的狀態詳解
- 07 深入Thread類—線程API精講
- 08 集體協作,什么最重要?溝通!—線程的等待和通知
- 09 使用多線程實現分工、解耦、緩沖—生產者、消費者實戰
- 第3章 并發的問題和原因詳解
- 10 有福同享,有難同當—原子性
- 11 眼見不實—可見性
- 12 什么?還有這種操作!—有序性
- 13 問題的根源—Java內存模型簡介
- 14 僵持不下—死鎖詳解
- 第4章 如何解決并發問題
- 15 原子性輕量級實現—深入理解Atomic與CAS
- 16 讓你眼見為實—volatile詳解
- 17 資源有限,請排隊等候—Synchronized使用、原理及缺陷
- 18 線程作用域內共享變量—深入解析ThreadLocal
- 第5章 線程池
- 19 自己動手豐衣足食—簡單線程池實現
- 20 其實不用造輪子—Executor框架詳解
- 第6章 主要并發工具類
- 21 更高級的鎖—深入解析Lock
- 22 到底哪把鎖更適合你?—synchronized與ReentrantLock對比
- 23 按需上鎖—ReadWriteLock詳解
- 24 經典并發容器,多線程面試必備—深入解析ConcurrentHashMap上
- 25 經典并發容器,多線程面試必備—深入解析ConcurrentHashMap下
- 26不讓我進門,我就在門口一直等!—BlockingQueue和ArrayBlockingQueue
- 27 倒數計時開始,三、二、一—CountDownLatch詳解
- 28 人齊了,一起行動—CyclicBarrier詳解
- 29 一手交錢,一手交貨—Exchanger詳解
- 30 限量供應,不好意思您來晚了—Semaphore詳解
- 第7章 高級并發工具類及并發設計模式
- 31 憑票取餐—Future模式詳解
- 32 請按到場順序發言—Completion Service詳解
- 33 分階段執行你的任務-學習使用Phaser運行多階段任務
- 34 誰都不能偷懶-通過 CompletableFuture 組裝你的異步計算單元
- 35 拆分你的任務—學習使用Fork/Join框架
- 36 為多線程們安排一位經理—Master/Slave模式詳解
- 第8章 總結
- 37 結束語