前言
死鎖是并發程式設計中的常見問題,它發生在兩個或多個線程被阻塞,等待對方釋放鎖時。死鎖可能導緻整個系統當機或崩潰,是一個難以複現和修複的問題。在本文中,我們将探讨 Java 中死鎖的成因、檢測方法以及避免死鎖的最佳實踐。
什麼是死鎖?
Java中的死鎖是當兩個或多個線程被阻塞并等待對方釋放資源,這種情況叫做死鎖。換句話說,兩個或多個線程被卡住而無法繼續,因為每個線程都持有另一個線程所需的資源,進而導緻循環依賴。這可能會導緻系統完全當機或崩潰。
例如,考慮兩個線程,線程 A 和線程 B,以及兩個鎖,鎖 1 和鎖 2。線程 A 擷取鎖 1,線程 B 擷取鎖 2。但是,線程 A 需要鎖 2 才能繼續,而線程 B 需要 鎖 1 才能繼續執行,該鎖正被線程 A 持有。這導緻循環依賴,兩個線程都被阻塞并等待另一個線程釋放鎖。這種情況稱為死鎖。
我們直接看一個代碼:
package core.multithreading;
public class DeadlockExample {
public static Object lock1 = new Object();
public static Object lock2 = new Object();
public static void main(String[] args) {
Thread threadA = new Thread(() -> {
synchronized(lock1) {
System.out.println("Thread A acquired lock 1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
synchronized(lock2) {
System.out.println("Thread A acquired lock 2");
}
}
});
Thread threadB = new Thread(() -> {
synchronized(lock2) {
System.out.println("Thread B acquired lock 2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
synchronized(lock1) {
System.out.println("Thread B acquired lock 1");
}
}
});
threadA.start();
threadB.start();
}
}
在這個例子中,我們有兩個線程,threadA 和 threadB,它們都通路兩個鎖,lock1 和 lock2。threadA先獲得lock1,然後threadB獲得lock2,兩個線程都休眠一秒。然後threadA嘗試擷取threadB持有的lock2,threadB嘗試擷取threadA持有的lock1。這導緻循環依賴,兩個線程都被阻塞并等待另一個線程釋放鎖,進而導緻死鎖。
為了避免這樣的死鎖,您可以遵循并發程式設計的最佳實踐,例如以固定順序擷取鎖、在擷取鎖時使用逾時、最小化鎖的範圍以及使用juc包中的ReentrantLock。
如何在 Java 中檢測死鎖?
檢測死鎖可能是一項具有挑戰性的任務,因為系統似乎已當機或無響應,而且不清楚問題出在哪裡。幸運的是,Java 提供了内置工具來檢測和診斷死鎖。
- dump線程資訊
線程dump分析可用于檢測 Java 中的死鎖。線程轉儲是在特定時間點在 Java 虛拟機 (JVM) 中運作的所有線程的狀态快照。通過分析線程轉儲,您可以檢測是否發生了死鎖。線上程轉儲中,您可以查找因等待鎖而被阻塞的線程,并确定哪些鎖由哪些線程持有。如果您在鎖定順序中看到循環依賴,這是潛在死鎖的迹象。
下面是一個顯示潛在死鎖的線程轉儲示例:
"Thread 1" - waiting to lock monitor on Lock 1
"Thread 2" - waiting to lock monitor on Lock 2
Found 1 deadlock.
- JConsole
JConsole 是一個 Java 管理擴充 (JMX) 用戶端,允許您監視和管理 Java 應用程式。您可以使用 JConsole 通過檢查 Threads 頁籤來檢測死鎖。如果有線程被阻塞并等待鎖,它會顯示在“Thread State”列中,值為“BLOCKED”。
下面是顯示阻塞線程的 JConsole 示例:
Name: Thread-1
State: BLOCKED on Lock 1
- VisualVM
VisualVM 是另一個允許您監視和管理 Java 應用程式的工具。與 JConsole 一樣,您可以使用 VisualVM 通過檢查線程頁籤來檢測死鎖。如果有線程被阻塞并等待鎖,它會顯示在“State”列中,值為“BLOCKED”。
下面是顯示阻塞線程的 VisualVM 示例:
Name: Thread-1
State: BLOCKED on Lock 1 owned by Thread-2
- LockSupport
LockSupport 類提供一組可用于檢測死鎖的靜态方法。其中一個方法是 parkNanos(),它可用于檢查線程是否被阻塞并等待鎖。如果 parkNanos() 傳回 true,則意味着線程被阻塞,并且存在潛在的死鎖。
下面是使用 LockSupport 檢測潛在死鎖的示例:
Thread t = Thread.currentThread();
LockSupport.parkNanos(1000000000);
if (t.getState() == Thread.State.BLOCKED) {
// Potential deadlock
}
避免死鎖的最佳實踐
- 以固定順序擷取鎖
為避免循環依賴鍊,您應該以固定順序擷取鎖。這意味着如果兩個或多個線程需要擷取多個鎖,它們應該總是以相同的順序擷取它們。例如,如果線程 A 擷取鎖 X,然後擷取鎖 Y,則線程 B 應該先擷取鎖 X,然後再嘗試擷取鎖 Y。
下面是一個以固定順序擷取鎖以避免循環依賴的示例代碼:
package core.multithreading;
public class DeadlockExample {
public static Object lock1 = new Object();
public static Object lock2 = new Object();
public static void main(String[] args) {
Thread threadA = new Thread(() -> {
synchronized(lock1) {
System.out.println("Thread A acquired lock 1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
synchronized(lock2) {
System.out.println("Thread A acquired lock 2");
}
}
});
Thread threadB = new Thread(() -> {
synchronized(lock1) {
System.out.println("Thread B acquired lock 2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
synchronized(lock2) {
System.out.println("Thread B acquired lock 1");
}
}
});
threadA.start();
threadB.start();
}
}
在這個例子中,我們有兩個線程,每個線程調用一個方法,以固定順序擷取兩個鎖(lock1 和 lock2)。兩種方法擷取鎖的順序相同:首先是 lock1,然後是 lock2。這確定了鎖之間沒有循環依賴。
- 擷取鎖時使用逾時
為避免死鎖,您可以在擷取鎖時使用逾時。這意味着如果在指定時間内無法擷取鎖,線程将釋放鎖并稍後重試。
package core.multithreading;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockTimeoutExample {
private final Lock lock1 = new ReentrantLock();
private final Lock lock2 = new ReentrantLock();
public void method1() {
boolean lock1Acquired = false;
boolean lock2Acquired = false;
try {
System.out.println("Thread 1: Attempting to acquire lock1");
lock1Acquired = lock1.tryLock(500, TimeUnit.MILLISECONDS);
System.out.println("Thread 1: Acquired lock1 = " + lock1Acquired);
System.out.println("Thread 1: Attempting to acquire lock2");
lock2Acquired = lock2.tryLock(500, TimeUnit.MILLISECONDS);
System.out.println("Thread 1: Acquired lock2 = " + lock2Acquired);
if (lock1Acquired && lock2Acquired) {
// Do something
} else {
// Locks not acquired
}
} catch (InterruptedException e) {
// Handle the exception
} finally {
if (lock1Acquired) {
lock1.unlock();
System.out.println("Thread 1: Released lock1");
}
if (lock2Acquired) {
lock2.unlock();
System.out.println("Thread 1: Released lock2");
}
}
}
public void method2() {
boolean lock1Acquired = false;
boolean lock2Acquired = false;
try {
System.out.println("Thread 2: Attempting to acquire lock2");
lock2Acquired = lock2.tryLock(500, TimeUnit.MILLISECONDS);
System.out.println("Thread 2: Acquired lock2 = " + lock2Acquired);
System.out.println("Thread 2: Attempting to acquire lock1");
lock1Acquired = lock1.tryLock(500, TimeUnit.MILLISECONDS);
System.out.println("Thread 2: Acquired lock1 = " + lock1Acquired);
if (lock1Acquired && lock2Acquired) {
// Do something
} else {
// Locks not acquired
}
} catch (InterruptedException e) {
// Handle the exception
} finally {
if (lock1Acquired) {
lock1.unlock();
System.out.println("Thread 2: Released lock1");
}
if (lock2Acquired) {
lock2.unlock();
System.out.println("Thread 2: Released lock2");
}
}
}
public static void main(String[] args) {
LockTimeoutExample example = new LockTimeoutExample();
Thread t1 = new Thread(new Runnable() {
public void run() {
example.method1();
}
});
Thread t2 = new Thread(new Runnable() {
public void run() {
example.method2();
}
});
t1.start();
t2.start();
}
}
在這個例子中,我們建立了一個 DeadlockExample 類的執行個體,并啟動了兩個線程,一個運作 method1(),另一個運作 method2()。每個方法都嘗試以不同的順序擷取兩個鎖,這應該可以防止發生任何死鎖。
- 最小化鎖的範圍
為避免死鎖,您應該盡量減少鎖的範圍。這意味着您應該隻在必要時擷取鎖并盡快釋放它。這可以通過使用同步塊而不是同步方法來實作。同步塊允許您明确指定鎖的範圍。
package core.multithreading;
public class SynchronizedLockExample {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
public void method1() {
synchronized (lock1) {
System.out.println("method1: lock1 acquired");
try {
Thread.sleep(1000); // simulate some work
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("method1: lock2 acquired");
// Do something
}
}
}
public void method2() {
synchronized (lock1) {
System.out.println("method2: lock1 acquired");
try {
Thread.sleep(1000); // simulate some work
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("method2: lock2 acquired");
// Do something
}
}
}
public static void main(String[] args) {
SynchronizedLockExample example = new SynchronizedLockExample();
Thread t1 = new Thread(new Runnable() {
public void run() {
example.method1();
}
});
Thread t2 = new Thread(new Runnable() {
public void run() {
example.method2();
}
});
t1.start();
t2.start();
}
}
在method1和method2中,synchronized塊分别用于擷取lock1和lock2上的鎖。在main方法中,建立了兩個線程來調用這兩個方法。當線程開始運作時,一個線程将擷取 lock1 上的鎖,另一個線程将等待直到鎖被釋放。一旦鎖被釋放,等待線程就會擷取到鎖,繼續執行method2内部的synchronized塊。
總結
死鎖是并發程式設計中的常見問題,可能導緻系統完全當機或崩潰。檢測和修複死鎖可能是一項具有挑戰性的任務,但 Java 提供了内置工具來檢測和診斷死鎖。為避免死鎖,您應該以固定順序擷取鎖,在擷取鎖時使用逾時,最小化鎖的範圍。通過遵循這些最佳實踐,您可以降低死鎖的風險并確定您的并發程式順利運作。