破壞死鎖的四個條件
銀行家演算法
按順序請求資源
儘量避免巢狀鎖
嘗試鎖定trylock
避免死鎖 是併發程式設計中的一個重要問題。死鎖是指多個執行緒在等待彼此持有的資源,導致無法繼續執行的狀態。在 Java 中,死鎖通常發生在多執行緒程式中,尤其是在使用同步塊、鎖和其他併發機制時。
避免死鎖有幾種常見的策略和技術,下面詳細介紹:
1. 資源申請順序一致(避免迴圈等待)
問題: 死鎖的一個重要條件是執行緒在不同的順序上請求資源,導致迴圈等待的情況發生。
解決方法: 透過規定資源獲取的順序,使所有執行緒都按照相同的順序請求鎖或資源,避免產生迴圈等待。
示例:
class Resource1 {}
class Resource2 {}
public class DeadlockAvoidance {
private final Resource1 r1 = new Resource1();
private final Resource2 r2 = new Resource2();
public void method1() {
synchronized (r1) {
synchronized (r2) {
// 對資源 r1 和 r2 的操作
System.out.println("Method 1");
}
}
}
public void method2() {
synchronized (r1) { // 保持與 method1 相同的鎖獲取順序
synchronized (r2) {
// 對資源 r1 和 r2 的操作
System.out.println("Method 2");
}
}
}
public static void main(String[] args) {
DeadlockAvoidance da = new DeadlockAvoidance();
new Thread(da::method1).start();
new Thread(da::method2).start();
}
}
解釋: 在 method1()
和 method2()
中,兩個執行緒獲取資源的順序是相同的(先 r1
,再 r2
)。如果每個執行緒都按照相同的順序獲取資源,迴圈等待就不會發生,從而避免了死鎖。
2. 嘗試鎖定(使用 tryLock
)
問題: 當執行緒獲取多個鎖時,如果某個鎖被佔用,執行緒會一直等待,可能造成死鎖。
解決方法: 使用 java.util.concurrent.locks.Lock
介面提供的 tryLock()
方法,它允許執行緒嘗試獲取鎖。如果鎖不可用,執行緒可以選擇不等待,而是執行其他邏輯,避免死鎖。
示例:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TryLockExample {
private final Lock lock1 = new ReentrantLock();
private final Lock lock2 = new ReentrantLock();
public void tryLockMethod1() {
try {
if (lock1.tryLock() && lock2.tryLock()) {
try {
// 對資源的操作
System.out.println("Method 1 acquired both locks");
} finally {
lock2.unlock();
lock1.unlock();
}
} else {
System.out.println("Method 1 failed to acquire locks");
}
} finally {
// 釋放鎖的操作
}
}
public void tryLockMethod2() {
try {
if (lock2.tryLock() && lock1.tryLock()) {
try {
// 對資源的操作
System.out.println("Method 2 acquired both locks");
} finally {
lock1.unlock();
lock2.unlock();
}
} else {
System.out.println("Method 2 failed to acquire locks");
}
} finally {
// 釋放鎖的操作
}
}
public static void main(String[] args) {
TryLockExample example = new TryLockExample();
new Thread(example::tryLockMethod1).start();
new Thread(example::tryLockMethod2).start();
}
}
解釋: 透過 tryLock()
方法,執行緒嘗試獲取鎖。如果鎖不可用,它可以立即返回 false
,然後執行其他操作而不是一直等待,從而避免了死鎖的發生。
3. 超時獲取鎖
問題: 在某些情況下,執行緒可能永遠無法獲取到鎖,導致死鎖。
解決方法: 使用帶有超時的 tryLock(long timeout, TimeUnit unit)
方法來防止無限期等待鎖。執行緒嘗試在給定的時間內獲取鎖,如果超時,它將放棄對鎖的請求,從而減少死鎖的可能性。
示例:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;
public class LockTimeoutExample {
private final Lock lock1 = new ReentrantLock();
private final Lock lock2 = new ReentrantLock();
public void method1() {
try {
if (lock1.tryLock(1000, TimeUnit.MILLISECONDS)) {
try {
if (lock2.tryLock(1000, TimeUnit.MILLISECONDS)) {
try {
// 對資源的操作
System.out.println("Method 1 acquired both locks");
} finally {
lock2.unlock();
}
} else {
System.out.println("Method 1 failed to acquire lock2");
}
} finally {
lock1.unlock();
}
} else {
System.out.println("Method 1 failed to acquire lock1");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void method2() {
try {
if (lock2.tryLock(1000, TimeUnit.MILLISECONDS)) {
try {
if (lock1.tryLock(1000, TimeUnit.MILLISECONDS)) {
try {
// 對資源的操作
System.out.println("Method 2 acquired both locks");
} finally {
lock1.unlock();
}
} else {
System.out.println("Method 2 failed to acquire lock1");
}
} finally {
lock2.unlock();
}
} else {
System.out.println("Method 2 failed to acquire lock2");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
LockTimeoutExample example = new LockTimeoutExample();
new Thread(example::method1).start();
new Thread(example::method2).start();
}
}
解釋: 如果執行緒無法在指定的超時時間內獲取到鎖,它將放棄鎖的請求,避免死鎖的產生。
4. 減少鎖的使用範圍
問題: 死鎖通常發生在鎖持有的時間過長或鎖的範圍過大時。
解決方法: 只在真正需要保護共享資源時獲取鎖,儘量減少鎖的使用範圍(即縮小同步塊的範圍),從而減少產生死鎖的機率。
示例:
public class NarrowLockScope {
private final Object lock = new Object();
public void doSomething() {
// 在非關鍵程式碼區不使用鎖
System.out.println("Doing something outside lock");
synchronized (lock) {
// 僅在真正需要同步時使用鎖
System.out.println("Doing something with lock");
}
}
}
解釋: 鎖只用於保護共享資源的關鍵部分,減少了持有鎖的時間,降低了發生死鎖的可能性。
5. 避免巢狀鎖
問題: 巢狀鎖的使用容易導致死鎖。即一個執行緒在持有鎖 A 的同時,試圖去獲取鎖 B,另一執行緒在持有鎖 B 時試圖獲取鎖 A,這可能導致死鎖。
解決方法: 避免巢狀鎖,儘量保持獲取單個鎖的原則。
示例:
public class SingleLockExample {
private final Object lock = new Object();
public void doSomething() {
synchronized (lock) {
// 執行需要同步的操作
System.out.println("Doing something");
}
}
}
解釋: 使用一個鎖,避免多個執行緒同時競爭多個鎖,避免了巢狀鎖帶來的死鎖問題。
6. 使用高階併發工具
問題: 手動管理鎖的使用容易導致程式設計複雜性增加,且容易引發死鎖問題。
解決方法: 使用 Java 提供的高階併發工具類,如 java.util.concurrent
包中的 Semaphore
、CountDownLatch
、CyclicBarrier
和 ConcurrentHashMap
等來避免死鎖。這些工具類通常經過良好的設計和最佳化,可以幫助簡化併發程式碼,降低死鎖風險。
示例:
Semaphore semaphore = new Semaphore(1);
public void doSomething() {
try {
semaphore.acquire(); // 獲取訊號量
// 執行需要同步的操作
System.out.println("Doing something with semaphore");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 釋放訊號量
}
}
解釋: 使用 Semaphore
可以有效管理資源的併發訪問,避免傳統鎖機制引發的死鎖問題。
總結
為了避免死鎖,可以採取以下策略:
- 確保資源獲取的順序一致,避免迴圈等待。
- 使用
tryLock()
或設定