[Java併發]避免死鎖

Duancf發表於2024-09-28

破壞死鎖的四個條件
銀行家演算法
按順序請求資源
儘量避免巢狀鎖
嘗試鎖定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 包中的 SemaphoreCountDownLatchCyclicBarrierConcurrentHashMap 等來避免死鎖。這些工具類通常經過良好的設計和最佳化,可以幫助簡化併發程式碼,降低死鎖風險。

示例:

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() 或設定

相關文章