死鎖及解決方案

文章首發於公衆號,歡迎訂閱
在這裏插入圖片描述

什麼是死鎖

每個線程都在等待對方線程釋放鎖,然而誰都不主動釋放鎖,結果就構成死鎖。

死鎖的影響在不同系統中是不一樣的,這取決於系統對死鎖的處理能力。

數據庫:檢測並放棄事務。

JVM :無法自動處理。

發生死鎖的例子

經典死鎖

public class DeadLock implements Runnable {

    int flag = 1;

    static Object lock1 = new Object();
    static Object lock2 = new Object();

    public static void main(String[] args) {
        DeadLock deadLock1 = new DeadLock();
        DeadLock deadLock2 = new DeadLock();
        deadLock1.flag = 1;
        deadLock2.flag = 0;
        Thread t1 = new Thread(deadLock1);
        Thread t2 = new Thread(deadLock2);
        t1.start();
        t2.start();
    }

    @Override
    public void run() {
        System.out.println("flag = " + flag);
        if (flag == 1) {
            synchronized (lock1) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2) {
                    System.out.println("線程1成功拿到兩把鎖");
                }
            }
        }
        if (flag == 0) {
            synchronized (lock2) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock1) {
                    System.out.println("線程2成功拿到兩把鎖");
                }
            }
        }
    }
}

由於 lock1 在等 lock2 釋放鎖,而 lock2 在等 lock1 釋放鎖,從而構成了死鎖。

轉賬

public class TransferMoney implements Runnable {

    int flag = 1;
    static Account a = new Account(500);
    static Account b = new Account(500);

    public static void main(String[] args) throws InterruptedException {
        TransferMoney r1 = new TransferMoney();
        TransferMoney r2 = new TransferMoney();
        r1.flag = 1;
        r2.flag = 0;
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("a的餘額" + a.balance);
        System.out.println("b的餘額" + b.balance);
    }

    @Override
    public void run() {
        if (flag == 1) {
            transferMoney(a, b, 200);
        }
        if (flag == 0) {
            transferMoney(b, a, 200);
        }
    }

    public static void transferMoney(Account from, Account to, int amount) {

        synchronized (from) {
            try {
                // 模擬耗時操作
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (to) {
                if (from.balance - amount < 0) {
                    System.out.println("餘額不足,轉賬失敗。");
                    return;
                }
                from.balance -= amount;
                to.balance = to.balance + amount;
                System.out.println("成功轉賬" + amount + "元");
            }
        }
    }

    static class Account {

        public Account(int balance) {
            this.balance = balance;
        }

        int balance;
    }
}

多人隨機轉賬

public class MultiTransferMoney {

    // 人越少,死鎖機率越大
    private static final int NUM_ACCOUNTS = 50;
    private static final int NUM_MONEY = 1000;
    private static final int NUM_ITERATIONS = 1000000;
    private static final int NUM_THREADS = 20;

    public static void main(String[] args) {

        Random rnd = new Random();
        TransferMoney.Account[] accounts = new TransferMoney.Account[NUM_ACCOUNTS];
        for (int i = 0; i < accounts.length; i++) {
            accounts[i] = new TransferMoney.Account(NUM_MONEY);
        }
        class TransferThread extends Thread {

            @Override
            public void run() {
                for (int i = 0; i < NUM_ITERATIONS; i++) {
                    int fromAcct = rnd.nextInt(NUM_ACCOUNTS);
                    int toAcct = rnd.nextInt(NUM_ACCOUNTS);
                    int amount = rnd.nextInt(NUM_MONEY);
                    TransferMoney.transferMoney(accounts[fromAcct], accounts[toAcct], amount);
                }
                System.out.println("運行結束");
            }
        }
        for (int i = 0; i < NUM_THREADS; i++) {
            new TransferThread().start();
        }
    }
}

死鎖的必要條件

死鎖的產生具備以下四個條件:

  • 互斥條件:指線程對己經獲取到的資源進行排它性使用, 即該資源同時只由一個線程佔用。如果此時還有其他線程請求獲取該資源,則請求者只能等待,直至佔有資源的線程釋放該資源。
  • 請求並持有條件: 指一個線程己經持有了至少一個資源 , 但又提出了新的資源請求 ,而新資源己被其他線程佔有,所以當前線程會被阻塞,但阻塞的同時並不釋放自己己經獲取的資源。
  • 不可剝奪條件: 指線程獲取到的資源在自己使用完之前不能被其他線程搶佔,只有在自己使用完畢後才由自己釋放該資源。
  • 環路等待條件:指在發生死鎖時,必然存在一個線程→資源的環形鏈,即線程集合{ T0,T1,T2 ,…,Tn }中的 T0 正在等待一個 T1 佔用的資源,T1 正在等待 T2 佔用的資源,……Tn 正在等待己被 T0 佔用的資源。

來看上面的經典死鎖示例是如何滿足這四個條件的。

首先,lock1 和 lock2 都是互斥資源,當線程 1 調用 synchronized(lock1) 方法獲取到 lock1 鎖並釋放前, 線程 2 再調用 synchronized(lock1) 方法嘗試獲取該資源會被阻塞,只有線程 1 主動釋放該鎖,線程 2 才能獲得,這滿足了資源互斥條件 。 線程 1 首先通過 synchronized(lock1) 方法獲取到 lock1 鎖,然後通過 synchronized(lock2) 方法等待獲取 lock2 鎖,這就構成了請求並持有條件。 線程 1 在獲取 lock1 鎖後,該資源不會被線程 2 掠奪走 , 只有線程 1 自己主動釋放 lock1 資源時,它纔會放棄對該資源的持有權 ,這構成了資源的不可剝奪條件 。 線程 1 持有 lock1 並等待獲取 lock2 ,而線程 2 持有 lcok2 資源並等待 lock1 資源,這構成了環路等待條件

如何定位死鎖

  • 利用 jstack 查看線程 pid
  • 利用 JMX 的 ThreadMXBean
public class ThreadMXBeanDetection implements Runnable {

    int flag = 1;

    static Object lock1 = new Object();
    static Object lock2 = new Object();

    public static void main(String[] args) throws InterruptedException {
        ThreadMXBeanDetection r1 = new ThreadMXBeanDetection();
        ThreadMXBeanDetection r2 = new ThreadMXBeanDetection();
        r1.flag = 1;
        r2.flag = 0;
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);
        t1.start();
        t2.start();
        Thread.sleep(1000);
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
        if (deadlockedThreads != null && deadlockedThreads.length > 0) {
            for (long deadlockedThread : deadlockedThreads) {
                ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThread);
                System.out.println("發現死鎖" + threadInfo.getThreadName());
            }
        }
    }

    @Override
    public void run() {
        System.out.println("flag = " + flag);
        if (flag == 1) {
            synchronized (lock1) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2) {
                    System.out.println("線程1成功拿到兩把鎖");
                }
            }
        }
        if (flag == 0) {
            synchronized (lock2) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock1) {
                    System.out.println("線程2成功拿到兩把鎖");
                }
            }
        }
    }
}

修復死鎖的策略

避免策略

改變獲取鎖的順序,避免相反的獲取鎖的順序

修改之前轉賬示例的代碼:

public class TransferMoney implements Runnable {

    int flag = 1;
    static Account a = new Account(500);
    static Account b = new Account(500);
    static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        TransferMoney r1 = new TransferMoney();
        TransferMoney r2 = new TransferMoney();
        r1.flag = 1;
        r2.flag = 0;
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("a的餘額" + a.balance);
        System.out.println("b的餘額" + b.balance);
    }

    @Override
    public void run() {
        if (flag == 1) {
            transferMoney(a, b, 200);
        }
        if (flag == 0) {
            transferMoney(b, a, 200);
        }
    }

    public static void transferMoney(Account from, Account to, int amount) {
        class Helper {

            public void transfer() {
                if (from.balance - amount < 0) {
                    System.out.println("餘額不足,轉賬失敗。");
                    return;
                }
                from.balance -= amount;
                to.balance = to.balance + amount;
                System.out.println("成功轉賬" + amount + "元");
            }
        }
        // 通過hashCode來決定獲取鎖的順序
        int fromHash = System.identityHashCode(from);
        int toHash = System.identityHashCode(to);
        if (fromHash < toHash) {
            synchronized (from) {
                synchronized (to) {
                    new Helper().transfer();
                }
            }
        } else if (fromHash > toHash) {
            synchronized (to) {
                synchronized (from) {
                    new Helper().transfer();
                }
            }
        } else {
            // 如果發生哈希衝突,則需要進行“加時賽”
            synchronized (lock) {
                synchronized (to) {
                    synchronized (from) {
                        new Helper().transfer();
                    }
                }
            }
        }
    }

    static class Account {

        public Account(int balance) {
            this.balance = balance;
        }

        int balance;
    }
}

爲了避免哈希衝突,可以使用主鍵,一般主鍵是唯一的。

檢測與修復策略

一段時間檢測是否有死鎖,如果有就剝奪某一個資源,來打開死鎖。

恢復方法1:進程終止

逐個終止線程,直到死鎖消除。

終止順序:

  1. 優先級(是前臺交互還是後臺處理)
  2. 已佔用資源、還需要的資源
  3. 已經運行時間

恢復方法2:資源搶佔

把已經分發出去的鎖給收回來。讓線程回退幾步,這樣就不用結束整個線程,成本比較低。

缺點:可能同一個線程一直被搶佔,那就造成飢餓

鴕鳥策略

鴕鳥這種動物在遇到危險的時候,通常就會把頭埋在地上,這樣一來它就看不到危險了。而蛇鳥策略的意思就是說,如果我們發生死鎖的概率極其低,那麼我們就直接忽略它,直到死鎖發生的時候,再人工修復。

如何避免死鎖

1、設置超時時間,利用LocktryLock(long timeout, TimeUnit unit)方法。

public class TryLockDeadlock implements Runnable {

    int flag = 1;
    static Lock lock1 = new ReentrantLock();
    static Lock lock2 = new ReentrantLock();

    public static void main(String[] args) {
        TryLockDeadlock r1 = new TryLockDeadlock();
        TryLockDeadlock r2 = new TryLockDeadlock();
        r1.flag = 1;
        r2.flag = 0;
        new Thread(r1).start();
        new Thread(r2).start();
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (flag == 1) {
                try {
                    if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
                        System.out.println("線程1獲取到了鎖1");
                        Thread.sleep(new Random().nextInt(1000));
                        if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
                            System.out.println("線程1獲取到了鎖2");
                            System.out.println("線程1成功獲取到了兩把鎖");
                            lock2.unlock();
                            lock1.unlock();
                            break;
                        } else {
                            System.out.println("線程1嘗試獲取鎖2失敗,已重試");
                            lock1.unlock();
                            Thread.sleep(new Random().nextInt(1000));
                        }
                    } else {
                        System.out.println("線程1獲取鎖1失敗,已重試");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            if (flag == 0) {
                try {
                    if (lock2.tryLock(3000, TimeUnit.MILLISECONDS)) {
                        System.out.println("線程2獲取到了鎖2");

                        Thread.sleep(new Random().nextInt(1000));
                        if (lock1.tryLock(3000, TimeUnit.MILLISECONDS)) {
                            System.out.println("線程2獲取到了鎖1");
                            System.out.println("線程2成功獲取到了兩把鎖");
                            lock1.unlock();
                            lock2.unlock();
                            break;
                        } else {
                            System.out.println("線程2嘗試獲取鎖1失敗,已重試");
                            lock2.unlock();
                            Thread.sleep(new Random().nextInt(1000));
                        }
                    } else {
                        System.out.println("線程2獲取鎖2失敗,已重試");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

2、多使用併發類而不是自己設計鎖。

3、儘量降低鎖的使用粒度:用不同的鎖而不是一個鎖。

4、如果能使用同步代碼塊,就不使用同步方法。

5、給你的線程起個有意義的名字:debug 和排查時事半功倍,框架和 JDK 都遵守這個最佳實踐。

6、避免鎖的嵌套。

7、分配資源前先看能不能收回來:銀行家算法。

8、儘量不要幾個功能用同一把鎖:專鎖專用。

活鎖

線程主動將資源釋放給他人使用,那麼就會導致資源不斷地在兩個線程間跳動,而沒有一個線程可以同時拿到所有資源正常執行,這種情況就是活鎖。

public class LiveLock {

    static class Spoon {

        private Diner owner;

        public Spoon(Diner owner) {
            this.owner = owner;
        }

        public Diner getOwner() {
            return owner;
        }

        public void setOwner(Diner owner) {
            this.owner = owner;
        }

        public synchronized void use() {
            System.out.printf("%s吃完了!", owner.name);


        }
    }

    static class Diner {

        private String name;
        private boolean isHungry;

        public Diner(String name) {
            this.name = name;
            isHungry = true;
        }

        public void eatWith(Spoon spoon, Diner spouse) {
            while (isHungry) {
                if (spoon.owner != this) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    continue;
                }
                
                if (spouse.isHungry) {
                    System.out.println(name + ": 親愛的" + spouse.name + "你先吃吧");
                    spoon.setOwner(spouse);
                    continue;
                }

                spoon.use();
                isHungry = false;
                System.out.println(name + ": 我吃完了");
                spoon.setOwner(spouse);

            }
        }
    }


    public static void main(String[] args) {
        Diner husband = new Diner("牛郎");
        Diner wife = new Diner("織女");

        Spoon spoon = new Spoon(husband);

        new Thread(new Runnable() {
            @Override
            public void run() {
                husband.eatWith(spoon, wife);
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                wife.eatWith(spoon, husband);
            }
        }).start();
    }
}

上述代碼會無限執行下去。

解決方案:加入隨機因素,使其退出互相謙讓。還有其他如以太網的指數退避算法。

// 隨機因素
Random random = new Random();
if (spouse.isHungry && random.nextInt(10) < 9) {
    System.out.println(name + ": 親愛的" + spouse.name + "你先吃吧");
    spoon.setOwner(spouse);
    continue;
}

工作中的活鎖:消息隊列

如果消息如果處理失敗,就放在隊列頭重試,那麼可能處理該消息一直失敗。雖然沒阻塞,但程序無法繼續。

解決方案:放在隊尾或者添加重試機制,比如重試超過一定次數,就把該任務放入數據庫,數據庫檢測到新的任務,那麼後續定時任務會嘗試繼續執行該任務。

飢餓

飢餓指某一個或者多個線程因爲種種原因無法獲得所需要的資源,導致一直無法執行。比如它的線程優先級可能太低,而高優先級的線程不斷搶佔它需要的資源,導致低優先級線程無法工作。與死鎖相比,飢餓還是有可能在未來一段時間內解決的(比如,高優先級的線程已經完成任務,不再瘋狂執行)。


我創建了一個免費的知識星球,用於分享知識日記,歡迎加入!
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章