徹底搞懂死鎖的前世今生

關於死鎖,你知道多少?

本文就什麼是死鎖?怎麼找到死鎖?怎麼解決死鎖?怎麼避免死鎖等問題展開分析,通過大量的代碼和案例演示向大家描述死鎖的前世今生。

死鎖是什麼,有什麼危害?

定義

  • 併發情況下,當兩個(或多個)線程(或進程)相互持有對方所需要的資源,又不主動釋放,導致所有人都無法繼續前進,程序無限阻塞,就是死鎖

兩個線程:
在這裏插入圖片描述
多個線程:
在這裏插入圖片描述

危害

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

    • 數據庫中:檢測並放棄事務
    • JVM中:無法自動處理
  • 死鎖的機率不高但是危害大

    • 一旦發生,多是高併發場景,影響用戶多
    • 整個系統崩潰,子系統崩潰,性能降低
    • 壓力測試無法找到所有的死鎖

寫一個死鎖的例子

案例一:一定會死鎖

第一個線程拿到鎖o1後等待500毫秒,這段時間第二個線程可以拿到鎖o2

然後線程1等待鎖o2,線程2等待鎖o1

造成程序無限阻塞的現象

代碼演示如下:

/**
 * 〈必定發生死鎖的現象〉
 *
 * @author Chkl
 * @create 2020/3/9
 * @since 1.0.0
 */
public class MustDeadLock implements Runnable {

    int flag = 1;

    static Object o1 = new Object();
    static Object o2 = new Object();


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

        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);

        t1.start();
        t2.start();

    }


    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"開始了,flag = " + flag);
        if (flag == 1) {
            synchronized (o1){
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o2){
                    System.out.println("線程1拿到兩把鎖");
                }
            }
        } else if (flag == 0) {
            synchronized (o2){
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o1){
                    System.out.println("線程2拿到兩把鎖");
                }
            }
        }

    }
}
案例二:兩個賬戶轉賬

模擬兩個賬戶進行轉賬

  • 如果線程獲得一個鎖後等待500毫秒,會出現和案例一的死鎖現象

  • 如果線程獲得一個鎖之後不等待500毫秒,只有很小的機率纔會發生死鎖,通常測試都會正常執行。

代碼演示如下:

/**
 * 〈轉賬時出現死鎖〉
 * 一旦註釋打開,發生死鎖
 *
 * @author Chkl
 * @create 2020/3/9
 * @since 1.0.0
 */
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) {
            //如果休眠500毫秒,那麼另一個線程就會拿到to鎖,造成相互等待的死鎖現象
//            try {
//                Thread.sleep(500);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
            synchronized (to) {
                if (from.balance - amount < 0) {
                    System.out.println("餘額不足,轉賬失敗!");
                } else {
                    from.balance -= amount;
                    to.balance += amount;
                    System.out.println("成功轉賬" + amount + "元");
                }
            }
        }
    }

    //賬戶對象,擁有屬性balance
    static class Account {

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

}
案例三:多人多次轉賬

如果兩個鎖之間不進行等待,很難發生死鎖

爲了驗證不等待也會發生死鎖,並且死鎖的發生是具有傳遞性的(而不是僅有少數鎖住其他正常運行的),下面我們來完成多人多次轉賬案例

設置500個賬戶,每個線程進行操作,並且每個線程轉賬100000次。每次轉賬的賬戶和金額都是隨機產生的

演示代碼如下:

/**
 * 〈模擬多人隨機轉賬〉
 *
 * @author Chkl
 * @create 2020/3/9
 * @since 1.0.0
 */
public class MultiTransferMoney {

    //賬戶數
    private static final int NUM_ACCOUNTS = 500;
    //賬戶金額
    private static final int NUM_MONEY = 1000;
    //每人轉賬次數
    private static final int NUM_ITERATIONS = 100000;
    //同時轉賬人數
    private static final int NUM_THREADS = 5000;

    public static void main(String[] args) {
        Random random = new Random();
        Account[] accounts = new Account[NUM_ACCOUNTS];

        for (int i = 0; i < accounts.length; i++) {
            accounts[i] = new Account(NUM_MONEY);
        }

        class TransferThread extends Thread {
            @Override
            public void run() {
                for (int i = 0; i < NUM_ITERATIONS; i++) {
                    int fromAcc = random.nextInt(NUM_ACCOUNTS);
                    int toAcc = random.nextInt(NUM_ACCOUNTS);
                    int amount = random.nextInt(NUM_MONEY);
                    transferMoney(accounts[fromAcc], accounts[toAcc], amount);
                }
                System.out.println("運行結束!");
            }

        }

        for (int i = 0;i<NUM_THREADS;i++){
            new TransferThread().start();
        }

    }


    public static void transferMoney(Account from, Account to, int amount) {
        synchronized (from) {
            synchronized (to) {
                if (from.balance - amount < 0) {
                    System.out.println("餘額不足,轉賬失敗!");
                } else {
                    from.balance -= amount;
                    to.balance += amount;
                    System.out.println("成功轉賬" + amount + "元");
                }
            }
        }
    }

    //賬戶對象,擁有屬性balance
    static class Account {

        int balance;

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

}

運行一段時間之後,死鎖的現象就出現了,控制檯沒有輸出“運行結束!”,並且進程也未結束。
在這裏插入圖片描述

驗證了依然會發生死鎖,並且死鎖具有傳遞性,並不是只有一兩個線程死鎖,而是所有線程都會被鎖死

發生死鎖必須滿足哪些條件

四個條件缺一不可:

  • 互斥條件

一個資源每一次只能被一個進程或者線程同時使用

  • 請求與保持條件

一個線程去請求一把鎖,同時它自身還保持一把鎖

請求的時候發生阻塞了,保持的鎖也不釋放

  • 不剝奪條件

沒有外界條件來剝奪一個鎖的擁有

  • 循環等待條件

各個鎖之間存在相互等待的情況,構成環

如何定位死鎖

  • jstack

    • 用命令行找到Java的pid(不同操作系統不同,詳細去百度吧)
    • 執行${JAVA_HOME}/bin/jstack pid,查找死鎖的信息
  • ThreadMXBean

在代碼中獲取是否發生死鎖,如果發生了就打印出信息

在線程啓動後,休眠一段時間等待進入死鎖,然後進行檢驗並打印

 	   //等1000毫秒,等它進入死鎖
        Thread.sleep(1000);
        ThreadMXBean threadMXBean =
                ManagementFactory.getThreadMXBean();
        long[] deadlockedThreads =
                threadMXBean.findDeadlockedThreads();
        //判斷是否有死鎖現象
        if (deadlockedThreads != null && deadlockedThreads.length > 0) {
            for (int i = 0; i < deadlockedThreads.length; i++) {
                ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThreads[i]);
                System.out.println("發現死鎖:"+threadInfo.getThreadName());
            }
        }

在多人多次轉賬的案例中進行檢查,運行結果如下

圖還沒貼

有哪些解決死鎖問題的策略?

線上發生死鎖怎麼辦
  • 保存案發現場後立刻重啓服務器

  • 暫時保證線上服務的安全,然後在利用剛纔保存的信息,排查死鎖,修改代碼,重新發版

常見修復策略
  1. 避免策略

    • 思路:避免相反的獲取鎖的順序
    • 演示:將之前的兩個賬戶轉賬的代碼進行修改,將transferMoney方法代碼修改如下

每次加鎖前判斷兩個鎖的hash值,如果兩個hash值不相等,都是先獲取hash值小的鎖,再獲取hash值大的鎖;如果發送hash衝突,就再加一把鎖鎖住加鎖的過程。保證無論什麼順序進行轉賬,都不會發生死鎖

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

        class Helper {
            public void transfer() {
                if (from.balance - amount < 0) {
                    System.out.println("餘額不足,轉賬失敗!");
                } else {
                    from.balance -= amount;
                    to.balance += amount;
                    System.out.println("成功轉賬" + amount + "元");
                }
            }
        }
        //獲取對象的hash值
        int fromHash = System.identityHashCode(from);
        int toHash = System.identityHashCode(to);

        //通過hash大小的比較,保證獲取鎖的順序是一定的
        //如果兩個賬戶相互轉賬,都是先加hash值小的鎖,保證了兩次加鎖的順序一致,就不會有死鎖了
        if (fromHash < toHash) {
            synchronized (from) {
                synchronized (to) {
                    new Helper().transfer();
                }
            }
        } else if (fromHash > toHash) {
            synchronized (to) {
                synchronized (from) {
                    new Helper().transfer();
                }
            }
         //hash衝突發生了
        } else {
            synchronized (lock) {
                synchronized (from) {
                    synchronized (to) {
                        new Helper().transfer();
                    }
                }
            }
        }

    }
  1. 檢測與恢復策略
  • 允許發生死鎖
  • 每次調用鎖都記錄在有向圖中
  • 定期檢查“鎖的調用鏈路圖”中是否存在環路
  • 一旦發生死鎖,調用死鎖恢復機制
    • 線程終止
      逐個終止線程,直到死鎖解除,順序如下:
      • 優先級(前臺交互還是後臺處理)
      • 已佔用資源和還需要的資源
      • 已運行時間
    • 資源搶佔
      • 發出去的鎖收回來,讓線程回退幾步
      • 缺點:可能同一個線程一直被搶佔,造成飢餓
  1. 鴕鳥策略

如果死鎖發送的機率非常低,那麼我們就直接忽略它,知道死鎖發送的時候,再人工修復

哲學家就餐問題

在這裏插入圖片描述

問題描述

假設有五位哲學家圍坐在一張圓形餐桌旁,做以下兩件事情之一:喫飯,或者思考。喫東西的時候,他們就停止思考,思考的時候也停止喫東西。餐桌中間有一大碗麪,每兩個哲學家之間有筷子。吃麪需要兩支筷子,所以假設哲學家必須用兩隻筷子喫東西。他們只能使用自己左右手邊的那兩隻筷子。

就餐流程

  • 先拿起左手的筷子
  • 然後拿起右手的筷子
  • 如果筷子被人使用了,那就等別人用完
  • 喫完後,把筷子放回原位
代碼演示
/**
 * 〈演示哲學家就餐問題導致的死鎖〉
 *
 * @author Chkl
 * @create 2020/3/9
 * @since 1.0.0
 */
public class DiningPhilosophers {

    public static class Philosopher implements Runnable {
        private Object leftChopstick;
        private Object rightChopstick;

        public Philosopher(Object leftChopstick, Object rightChopstick) {
            this.leftChopstick = leftChopstick;
            this.rightChopstick = rightChopstick;
        }

        @Override
        public void run() {
            try {
                while (true) {
                    doAction("Think");
                    synchronized (leftChopstick) {
                        doAction("picked up left chopstick");
                        synchronized (rightChopstick) {
                            doAction("picked up right chopstick");
                            doAction("put down right chopstick");
                        }
                        doAction("put down left chopstick");
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }


        }
    }

    private static void doAction(String action) throws InterruptedException {
        //打印操作
        System.out.println(Thread.currentThread().getName() + " " + action);
        //隨機休息
        Thread.sleep((long) Math.random() * 100);

    }

    public static void main(String[] args) {
        //定義哲學家
        Philosopher[] philosophers = new Philosopher[5];
        //定義筷子
        Object[] chopticks = new Object[philosophers.length];
        //初始化筷子
        for (int i = 0; i < chopticks.length; i++) {
            chopticks[i] = new Object();
        }
        //初始化哲學家
        for (int i = 0; i < philosophers.length; i++) {
            Object leftChopstick = chopticks[i % philosophers.length];
            Object rightChopstick = chopticks[(i + 1) % philosophers.length];
            philosophers[i] = new Philosopher(leftChopstick, rightChopstick);
            new Thread(philosophers[i], "哲學家" + (i + 1)+"號 ").start();
        }
    }

}

可能的一種結果:
在這裏插入圖片描述
每個哲學家都拿起來左邊的筷子,然後都在等待右邊的筷子,進入循環等待的死鎖現象

多種解決方案
  • 服務員檢查(避免策略)
    由服務員進行判斷分配,如果發現可能會發生死鎖,不允許就餐
  • 改變一個哲學家拿叉子的順序(避免策略)
    改變其中一個拿的順序,破壞環路
  • 餐票(避免策略)
    喫飯必須拿餐票,餐票一共只有4張,喫完了回收
  • 領導調節(檢測與恢復策略)
    定時檢查,如果發生死鎖,隨機剝奪一個的筷子
改變一個哲學家拿叉子的順序的實現

只需要修改哲學家初始化代碼,將最後一個哲學家的拿筷子順序進行交換,將代碼

philosophers[i] = new Philosopher(rightChopstick, leftChopstick);

替換成

 if (i == philosophers.length - 1) {
     philosophers[i] = new Philosopher(rightChopstick, leftChopstick);
 } else {
     philosophers[i] = new Philosopher(leftChopstick, rightChopstick);
 }

工程中如何避免死鎖

  1. 設置超時時間,超時發警報
    • Lock的tryLock(long timeout,TimeUnit unit)
    • synchronized不具備嘗試鎖的能力
    • 造成超時的可能性很多,發生了死鎖,死循環,線程執行慢

代碼演示:

/**
 * 〈用trylock來避免死鎖〉
 *
 * @author Chkl
 * @create 2020/3/9
 * @since 1.0.0
 */
public class TryLockDeadLock implements Runnable {

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

    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 {
                    //嘗試鎖,超時時間800毫秒
                    if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
                        //隨機休眠下,造成每次不一樣
                        Thread.sleep(new Random().nextInt(1000));
                        System.out.println("線程1成功獲取鎖1");
                        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 {
                    //嘗試鎖,超時時間3000毫秒
                    if (lock2.tryLock(3000, TimeUnit.MILLISECONDS)) {
                        //隨機休眠下,造成每次不一樣
                        Thread.sleep(new Random().nextInt(1000));
                        System.out.println("線程2成功獲取鎖2");
                        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獲取鎖2失敗,已重試");
                            lock2.unlock();
                            //隨機休眠下,造成每次不一樣
                            Thread.sleep(new Random().nextInt(1000));
                        }
                    } else {
                        System.out.println("線程2獲取鎖1失敗,已重試");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

一次運行結果如下:
在這裏插入圖片描述
雖然互斥的拿到了鎖,但是獲取超時後自動釋放了,解決了死鎖的情況

  1. 多使用併發類而不是自己設計的類
  • ConcurrentHashMap、ConcurrentLinkedQueue、AtomicBoolean等
  • Java.util.concurrent.atomic中的方法
  • 多用併發集合少用同步集合
  1. 降低鎖的使用粒度:使用不同的鎖而不是一個鎖
  2. 如果能用同步代碼塊,就不用同步方法:自己指定鎖的對象
  3. 新建線程的時候最好起個有意義的名字,方便排查
  4. 避免鎖的嵌套實現
  5. 分配資源前先看看能不能收回來:銀行家算法
  6. 儘量不要幾個功能使用同一個鎖:專鎖專用

線程活性故障

常見的線程活性故障包括死鎖,活鎖與線程飢餓

  • 活鎖:任務或者執行者沒有被阻塞,由於某些條件沒有滿足,導致一直重複嘗試,失敗,嘗試,失敗。
  • 飢餓:一個或者多個線程因爲種種原因無法獲得所需要的資源,導致一直無法執行的狀態,在非公平調度模式下,會出現。
  • 活鎖與死鎖的區別在於活鎖並不是嘗試一次不能獲取鎖就阻塞了,而是動態的一直嘗試獲取鎖,並且有可能解開
  • Java 中導致飢餓的原因:
    • 高優先級線程吞噬所有的低優先級線程的 CPU 時間。
    • 線程被永久堵塞在一個等待進入同步塊的狀態,因爲其他線程總是能在它之前持續地對該同步塊進行訪問。
    • 線程在等待一個本身也處於永久等待完成的對象(比如調用這個對象的 wait 方法),因爲其他線程總是被持續地獲得喚醒

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