前言
死鎖(Deadlock),是併發編程中最需要考慮的問題之一,一般來說死鎖發生的概率相對較小,但是危害奇大。本篇主要講解死鎖相關的內容,包括死鎖形成的必要條件、危害、如何避免等等。
死鎖的定義
死鎖(英語:Deadlock),又譯爲死結,計算機科學名詞。當兩個以上的運算單元,雙方都在等待對方停止運行,以獲取系統資源,但是沒有一方提前退出時,就稱爲死鎖。在多任務操作系統中,操作系統爲了協調不同行程,能否獲取系統資源時,爲了讓系統運作,必須要解決這個問題。
具體到線程死鎖,也就是多個(大於等於2個)線程相互持有對方需要的資源,在沒有外界干擾的情況下,會永遠處於等待狀態。
必然死鎖的例子
先來看一個必然發生死鎖的例子,來直觀的感受一下死鎖:
/**
* 必然死鎖的例子
* @author sicimike
*/
public class DeadLock {
final private static Object lock1 = new Object();
final private static Object lock2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lock1) {
// 先獲取lock1
System.out.println("thread-1 get lock1");
try {
// 休眠200毫秒,讓thread-2獲取lock2
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
// 在lock1同步代碼中獲取lock2
System.out.println("thread-1 get lock2");
}
}
System.out.println("thread-1 finished");
}, "thread-1");
Thread thread2 = new Thread(() -> {
synchronized (lock2) {
// 先獲取lock2
System.out.println("thread-1 get lock2");
synchronized (lock1) {
// 在lock2同步代碼中獲取lock1
System.out.println("thread-2 get lock1");
}
}
System.out.println("thread-2 finished");
}, "thread-2");
thread1.start();
thread2.start();
}
}
執行結果:
thread-1 get lock1
thread-2 get lock2
例子共有6行輸出,但是隻輸出了2行。不僅如此,用IDE運行該代碼時,IDE永遠不會執行結束。
對於本次運行結果,thread-1首先獲取CPU時間片,開始執行,獲取鎖lock1,輸出thread-1 get lock1
,然後休眠200毫秒。在200毫秒內thread-2獲取CPU時間片,開始執行,獲取lock2,輸出thread-2 get lock2
。
此時tread-1必須獲取lock2才能繼續執行,執行完成才能釋放自己持有的lock1。而thread-2同理,想要繼續執行,必須先獲取thread-1持有的lock1,執行完成才能釋放lock2。就這樣,兩個線程發生了死鎖。導致後續的代碼都不會執行,之後的語句並不會輸出。
死鎖的危害
- 首先肯定是程序得不到正確的結果,因爲處於死鎖狀態的線程無法處理原先分配的任務。
- 死鎖會降低資源利用率,處於死鎖狀態的線程所持有的資源是不會釋放的,更不能被別的線程利用,所以會導致資源的利用率降低
- 可能導致新的死鎖產生,死鎖的線程持有的資源,可能是系統非常寶貴且有限的資源,其他線程獲取不到,依然可能會被死鎖,產生多米諾骨牌效應
死鎖產生的必要條件
死鎖的危害非常巨大,是併發編程必須要考慮的問題。不過好在死鎖的產生條件比較嚴苛,需要同時滿足四個必要條件:
- 互斥條件:一個資源同時最多能被一個線程持有
- 請求與保持條件:一個線程因請求其他資源而被阻塞時,不會釋放已持有的資源
- 不剝奪條件:線程執行完成之前,其他的線程不能搶佔該線程持有的資源
- 循環等待條件:多個線程請求的資源形成一個等待環
只要其中一個不滿足就不可能發生死鎖。
再回過頭來看看上文的實例是不是滿足這四個條件,thread-1和thread-2所需的資源是lock1和lock2,都是互斥鎖,滿足互斥條件;thread-1和thread-2被阻塞後不會釋放持有的鎖,滿足請求與保持條件;thread-1和thread-2都不能直接搶佔對方持有的鎖,滿足不剝奪條件;thread-1需要thread-2持有的lock2,而thread-2需要thread-1持有的lock1,滿足循環等待條件。
因爲只有2個線程,所以循環等待條件不是很明顯,可以把實例改成三個線程
/**
* 三個線程-必然死鎖的例子
* @author sicimike
*/
public class ThreeThreadDeadLock {
final private static Object lock1 = new Object();
final private static Object lock2 = new Object();
final private static Object lock3 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lock1) {
// 先獲取lock1
System.out.println("thread-1 get lock1");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
// 在lock1中獲取lock2
System.out.println("thread-1 get lock2");
}
}
System.out.println("thread-1 finished");
}, "thread-1");
Thread thread2 = new Thread(() -> {
synchronized (lock2) {
// 先獲取lock2
System.out.println("thread-2 get lock2");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock3) {
// 在lock2中獲取lock3
System.out.println("thread-2 get lock3");
}
}
System.out.println("thread-2 finished");
}, "thread-2");
Thread thread3 = new Thread(() -> {
synchronized (lock3) {
// 先獲取lock3
System.out.println("thread-3 get lock3");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
// 在lock3中獲取lock1
System.out.println("thread-3 get lock1");
}
}
System.out.println("thread-3 finished");
}, "thread-3");
thread1.start();
thread2.start();
thread3.start();
}
}
執行結果
thread-1 get lock1
thread-2 get lock2
thread-3 get lock3
同樣,程序也不會結束。
thread-1獲取lock1後還需要獲取lock2,thread-2獲取lock2後還需要lock3,thread-3獲取lock3後還需要獲取lock1,這樣就是循環等待條件,三個線程所需要的資源形成了一個環。
定位死鎖
主要講解三種方式來定位死鎖:jstack命令、jconsole工具、ThreadMXBean類,以上面兩個線程死鎖的實例演示。
-
jstack命令
先查看系統進程,jps(Java Virtual Machine Process Status Tool)是JDK提供的一個顯示當前所有java進程pid的命令,位於...\jdk1.8.0_101\bin
目錄C:\Users\Atao>jps 2272 10180 Jps 13956 RemoteMavenServer36 11032 Launcher 8488 DeadLock
可以很清楚的看到運行DeadLock.java類的進程pid(8488),再運行jstack命令,jstack是JDK提供的線程堆棧分析工具,使用該命令可以查看Java程序線程堆棧信息,位於
...\jdk1.8.0_101\bin
目錄C:\Users\Atao>jstack -F 8488 Attaching to process ID 8488, please wait... Debugger attached successfully. Server compiler detected. JVM version is 25.101-b13 Deadlock Detection: Found one Java-level deadlock: ============================= "thread-1": waiting to lock Monitor@0x00000000173a0628 (Object@0x00000000d64dc960, a java/lang/Object), which is held by "thread-2" "thread-2": waiting to lock Monitor@0x000000001739f188 (Object@0x00000000d64dc950, a java/lang/Object), which is held by "thread-1" Found a total of 1 deadlock. Thread 1: (state = BLOCKED) ......
很清楚的就能看到哪幾個線程發生了死鎖(現在應該知道爲什麼要給每個線程取一個有意義的名字了)
-
jconsole工具,位於
...\jdk1.8.0_101\bin
目錄
啓動jconsole
檢測死鎖
查看檢測結果
-
ThreadMXBean類
ThreadMXBean
是JDK自帶的類,位於java.lang.management
包中。是Java虛擬機線程的系統管理接口。以上文舉出的必然死鎖的例子爲例:public class DeadLock { final private static Object lock1 = new Object(); final private static Object lock2 = new Object(); public static void main(String[] args) { Thread thread1 = new Thread(() -> { synchronized (lock1) { // 先獲取lock1 System.out.println("thread-1 get lock1"); try { // 休眠200毫秒,讓thread-2獲取lock2 Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock2) { // 在lock1同步代碼中獲取lock2 System.out.println("thread-1 get lock2"); } } System.out.println("thread-1 finished"); }, "thread-1"); Thread thread2 = new Thread(() -> { synchronized (lock2) { // 先獲取lock2 System.out.println("thread-2 get lock2"); synchronized (lock1) { // 在lock2同步代碼中獲取lock1 System.out.println("thread-2 get lock1"); } } System.out.println("thread-2 finished"); }, "thread-2"); // 檢測死鎖的線程 Thread monitorThread = new Thread(() -> { while (true) { try { // 每隔2秒檢測一次 Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("死鎖線程信息:"); ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); long[] deadlockedThreads = threadMXBean.findDeadlockedThreads(); for (long thread : deadlockedThreads) { System.out.println(threadMXBean.getThreadInfo(thread)); } } }, "monitor-thread"); thread1.start(); thread2.start(); monitorThread.start(); } }
執行結果:
thread-1 get lock1 thread-2 get lock2 死鎖線程信息: "thread-2" Id=12 BLOCKED on java.lang.Object@793190d2 owned by "thread-1" Id=11 "thread-1" Id=11 BLOCKED on java.lang.Object@77f67184 owned by "thread-2" Id=12 死鎖線程信息: "thread-2" Id=12 BLOCKED on java.lang.Object@793190d2 owned by "thread-1" Id=11 "thread-1" Id=11 BLOCKED on java.lang.Object@77f67184 owned by "thread-2" Id=12 ......
死鎖的處理
既然知道了死鎖產生的四個必要條件,所以只需要破壞其中一個或者多個即可。
- 對於互斥條件,想要破壞它,就是能加共享鎖的就不要加獨佔鎖
- 對於請求與保持條件,可以設置超時時間,阻塞一段時間後,如果還未獲取到鎖,就釋放自己持有的鎖;或者死鎖發生時,調度者強行中斷某個死鎖的狀態,並釋放持有的資源
- 對於不剝奪條件,請求資源(鎖)時,使用可以響應中斷的鎖,例如
Lock.lockInterruptibly()
- 對於循環等待條件,這個條件相對來說是最好破壞的。只需要打破等待環即可。線程請求多把鎖的時候,做到按順序請求鎖(每個線程),這樣就不會形成等待環。
可以動手改造下上文中三個線程死鎖的例子,使三個線程均按照lock1->lock2->lock3的順序請求鎖。
最佳實踐
死鎖的處理策略總的來說有三種方式:
- 避免策略
- 檢測與修復
- 不處理(鴕鳥策略)
對於生產環境而言,死鎖的防大於治,也就是說應該將重點放在死鎖的避免上。在實際工作中養成良好的習慣,可以大大減少死鎖發生的概率,好的習慣總結如下:
- 爭搶鎖時設置超時,比如Lock的tryLock(long, unit)方法
- 多使用併發工具類,而不是自己設計鎖
- 儘量降低鎖的粒度
- 如果能使用同步代碼塊,就不使用同步方法:鎖的粒度更小;還可以自己指定鎖對象
- 給線程起有意義的名字:方便debug、日誌記錄
- 避免鎖的嵌套
- 儘量不要多個功能用同一把鎖:專鎖專用
- 分配資源前先看能不能收回來:銀行家算法
總結
死鎖像火災一樣:不可預測、蔓延迅速、危害大。編寫併發程序的時候,一定要特別關注。
擴展閱讀
- https://zh.wikipedia.org/zh-hans/銀行家算法
- https://zh.wikipedia.org/wiki/哲學家就餐問題