死鎖(Dead Lock)指的是兩個或兩個以上的運算單元(程序、執行緒或協程),互相持有對方所需的資源,導致它們都無法向前推進,從而導致永久阻塞的問題就是死鎖。
比如執行緒 1 擁有了鎖 A 的情況下試圖獲取鎖 B,而執行緒 2 又在擁有了鎖 B 的情況下試圖獲取鎖 A,這樣雙方就進入相互阻塞等待的情況,如下圖所示:
死鎖的程式碼實現如下:
public class DeadlockDemo { public static void main(String[] args) { Object lock1 = new Object(); Object lock2 = new Object(); Thread thread1 = new Thread(() -> { synchronized (lock1) { System.out.println("Thread 1 acquired lock1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock2) { System.out.println("Thread 1 acquired lock2"); } } }); Thread thread2 = new Thread(() -> { synchronized (lock2) { System.out.println("Thread 2 acquired lock2"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock1) { System.out.println("Thread 2 acquired lock1"); } } }); thread1.start(); thread2.start(); }}
在上面的示例中,我們建立了兩個鎖 lock1 和 lock2,並在兩個執行緒中分別獲取這兩個鎖,但是獲取的順序不同。當 thread1 獲取 lock1 後,它會在持有鎖 lock1 的情況下嘗試獲取 lock2,而當 thread2 獲取 lock2 後,它會在持有鎖 lock2 的情況下嘗試獲取 lock1。 如果這兩個執行緒啟動後,thread1 先獲取 lock1 並且在獲取 lock2 之前休眠,那麼 thread2 就會獲取 lock2,然後在嘗試獲取 lock1 時被阻塞。此時,thread1 就會在獲取 lock2 時被阻塞。兩個執行緒都在等待對方釋放鎖,從而形成了死鎖。
# 死鎖 4 大條件
死鎖的產生需要滿足以下 4 個條件:
互斥條件:指運算單元(程序、執行緒或協程)對所分配到的資源具有排它性,也就是說在一段時間內某個鎖資源只能被一個運算單元所佔用。
請求和保持條件:指運算單元已經保持至少一個資源,但又提出了新的資源請求,而該資源已被其它運算單元佔有,此時請求運算單元阻塞,但又對自己已獲得的其它資源保持不放。
不可剝奪條件:指運算單元已獲得的資源,在未使用完之前,不能被剝奪。
環路等待條件:指在發生死鎖時,必然存在運算單元和資源的環形鏈,即運算單元正在等待另一個運算單元佔用的資源,而對方又在等待自己佔用的資源,從而造成環路等待的情況。
只有以上 4 個條件同時滿足,纔會造成死鎖。
# 解決死鎖
死鎖的常用解決方案有以下兩個:
按照順序加鎖:嘗試讓所有執行緒按照同一順序獲取鎖,從而避免死鎖。
設定獲取鎖的超時時間:嘗試獲取鎖的執行緒在規定時間內沒有獲取到鎖,就放棄獲取鎖,避免因為長時間等待鎖而引起的死鎖。
# 死鎖排查工具
有一些工具可以幫助排查死鎖問題,常見的工具有以下幾個:
jstack:可以檢視 Java 應用程式的執行緒狀態和呼叫堆疊,可用於發現死鎖執行緒的狀態。
jconsole 和 JVisualVM:這些是 Java 自帶的監視工具,可以用於監視執行緒、記憶體、CPU 使用率等資訊,從而幫助排查死鎖問題。
Thread Dump Analyzer(TDA):是一個開源的執行緒轉儲分析器,可用於分析和診斷 Java 應用程式中的死鎖問題。
Eclipse TPTP:是一個開源的效能測試工具平臺,其中包含了一個名為 Thread Profiler 的工具,可以用於跟蹤執行緒執行時的資訊,從而診斷死鎖問題。