Java并發(fā)編程 死鎖與修復(fù)死鎖

1.死鎖是什么?有什么危害?

1.1 什么是死鎖
  • 發(fā)生在并發(fā)中
  • 互不相讓:當(dāng)兩個(gè)(或更多)線程(或進(jìn)程)相互持有對(duì)方所需要的資源,又不主動(dòng)釋放,導(dǎo)致所有人都無法繼續(xù)前進(jìn),導(dǎo)致程序陷入無盡的阻塞,這就是死鎖。
image.png
  • 多個(gè)線程造成死鎖的情況
image.png
1.2 死鎖的影響

死鎖的影響在不同系統(tǒng)中是不一樣的,這取決于系統(tǒng)對(duì)死鎖的處理能力

  • 數(shù)據(jù)庫中:檢測(cè)并放棄事務(wù)
  • JVM中:無法自動(dòng)處理
幾率不高但危害大
  • 不一定發(fā)生,但是遵守"墨菲定律"
  • 一旦發(fā)生,多是高并發(fā)場(chǎng)景,影響用戶多
  • 整個(gè)系統(tǒng)崩潰、子系統(tǒng)崩潰、性能降低
  • 壓力測(cè)試無法找出所有潛在的死鎖
1.3 發(fā)生死鎖的例子
必定發(fā)生死鎖的情況
/**
 * 描述:     必定發(fā)生死鎖的情況
 */
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("flag = " + flag);
        if (flag == 1) {
            synchronized (o1) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o2) {
                    System.out.println("線程1成功拿到兩把鎖");
                }
            }
        }
        if (flag == 0) {
            synchronized (o2) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o1) {
                    System.out.println("線程2成功拿到兩把鎖");
                }
            }
        }
    }
}
flag = 1
flag = 0
多個(gè)人相互轉(zhuǎn)賬
  • 需要兩把鎖
  • 獲取兩把鎖成功,且余額大于0,則扣除轉(zhuǎn)出人,增加收款人的余額,是原子操作
  • 順序相反導(dǎo)致死鎖
public class MultiTransferMoney {

    private static final int NUM_ACCOUNTS = 500;
    private static final int NUM_MONEY = 1000;
    private static final int NUM_ITERATIONS = 1000000;
    private static final int NUM_THREADS = 100;

    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("運(yùn)行結(jié)束");
            }
        }
        for (int i = 0; i < NUM_THREADS; i++) {
            new TransferThread().start();
        }
    }
}
public class TransferMoney implements Runnable {

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

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

            public void transfer() {
                if (from.balance - amount < 0) {
                    System.out.println("余額不足,轉(zhuǎn)賬失敗。");
                    return;
                }
                from.balance -= amount;
                to.balance = to.balance + amount;
                System.out.println("成功轉(zhuǎn)賬" + amount + "元");
            }
        }

        synchronized (from) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (to) {
                new Helper().transfer();
            }
        }
    }

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

    static class Account {

        int balance;

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

死鎖

image.png

程序停止輸出發(fā)生死鎖

2. 死鎖的4個(gè)必要條件:

  1. 互斥條件
  2. 請(qǐng)求與保持條件
  3. 不剝奪條件
  4. 循環(huán)等待條件

3. 如何定位死鎖

3.1.jstack定位死鎖

并發(fā)編程中的死鎖定位排查

image.png
image.png
3.2.ThreadMXBean定位死鎖
public class ThreadMXBeanDetection implements Runnable {

    int flag = 1;

    static Object o1 = new Object();
    static Object o2 = 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 (int i = 0; i < deadlockedThreads.length; i++) {
                ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThreads[i]);
                System.out.println("發(fā)現(xiàn)死鎖" + threadInfo.getThreadName());
            }
        }
    }

    @Override
    public void run() {
        System.out.println("flag = " + flag);
        if (flag == 1) {
            synchronized (o1) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o2) {
                    System.out.println("線程1成功拿到兩把鎖");
                }
            }
        }
        if (flag == 0) {
            synchronized (o2) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o1) {
                    System.out.println("線程2成功拿到兩把鎖");
                }
            }
        }
    }
}
flag = 0
flag = 1
發(fā)現(xiàn)死鎖Thread-1
發(fā)現(xiàn)死鎖Thread-0

4. 修復(fù)死鎖策略

線上發(fā)生死鎖應(yīng)該什么辦

  • 線上問題都需要防患與未然,不造成損失幾乎是不可能
  • 保存死鎖數(shù)據(jù),然后立刻重啟服務(wù)器
  • 暫時(shí)保證線上服務(wù)的安全,然后再利用剛才保存的信息,排查死鎖,修改代碼,重新發(fā)版

常見修復(fù)策略

4.1. 避免策略
示例1 修改兩人轉(zhuǎn)賬時(shí)獲取鎖的順序

經(jīng)過思考,我們可以發(fā)現(xiàn),其實(shí)轉(zhuǎn)賬時(shí),并不在乎兩把鎖的相對(duì)獲取順序。轉(zhuǎn)賬的時(shí)候,我們無論先獲取到轉(zhuǎn)出賬戶鎖對(duì)象,還是先獲取到轉(zhuǎn)入賬戶鎖對(duì)象,只要最終能拿到兩把鎖,就能進(jìn)行安全的操作。所以我們來調(diào)整一下獲取鎖的順序,使得先獲取的賬戶和該賬戶是“轉(zhuǎn)入”或“轉(zhuǎn)出”無關(guān),而是使用 HashCode 的值來決定順序,從而保證線程安全

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

            public void transfer() {
                if (from.balance - amount < 0) {
                    System.out.println("余額不足,轉(zhuǎn)賬失敗。");
                    return;
                }
                from.balance -= amount;
                to.balance = to.balance + amount;
                System.out.println("成功轉(zhuǎn)賬" + amount + "元");
            }
        }
        int fromHash = System.identityHashCode(from);
        int toHash = System.identityHashCode(to);
        if (fromHash < toHash) {
            synchronized (from) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (to) {
                    new Helper().transfer();
                }
            }
        }
        else if (fromHash > toHash) {
            synchronized (to) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (from) {
                    new Helper().transfer();
                }
            }
        }else  {
            synchronized (lock) {
                synchronized (to) {
                    synchronized (from) {
                        new Helper().transfer();
                    }
                }
            }
        }

    }

可以看到,我們會(huì)分別計(jì)算出這兩個(gè) Account 的 HashCode,然后根據(jù) HashCode 的大小來決定獲取鎖的順序。這樣一來,不論是哪個(gè)線程先執(zhí)行,不論是轉(zhuǎn)出還是被轉(zhuǎn)入,它獲取鎖的順序都會(huì)嚴(yán)格根據(jù) HashCode 的值來決定,那么大家獲取鎖的順序就一樣了,就不會(huì)出現(xiàn)獲取鎖順序相反的情況,也就避免了死鎖

總結(jié):通過hashcode來決定獲取鎖的順序、沖突時(shí)需要“加時(shí)賽”(再加一把鎖)獲取鎖
image.png
示例2 哲學(xué)家換手解決.改變一個(gè)哲學(xué)家拿筷子的順序.
/**
 * 描述:     演示哲學(xué)家就餐問題導(dǎo)致的死鎖
 */
public class DiningForPhilosophers {

    /**
     * 哲學(xué)家類
     */
    public static class Philosophers implements Runnable{

        private Object leftChopsticks;
        private Object rightChopsticks;

        public Philosophers(Object leftChopsticks, Object rightChopsticks) {
            this.leftChopsticks = leftChopsticks;
            this.rightChopsticks = rightChopsticks;
        }

        @Override
        public void run() {
            try {
                while(true){
                    action("思考......");
                    synchronized (leftChopsticks){
                        action("拿起左邊的筷子");
                        synchronized (rightChopsticks){
                            action("拿起右邊的筷子---吃飯");
                            action("放下右邊的筷子");
                        }
                        action("放下左邊的筷子");
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        public static void action(String action) throws InterruptedException {
            System.out.println(Thread.currentThread().getName()+"-->"+action);
            //隨機(jī)休眠,代表這個(gè)行為執(zhí)行的耗時(shí)
            Thread.sleep((long) (Math.random()*10));
        }
    };

    public static void main(String[] args) {
        Philosophers[] philosophers = new Philosophers[5];
        Object[] chopsticks = new Object[philosophers.length];
        //初始化筷子對(duì)象
        for (int i=0;i<chopsticks.length;i++){
            chopsticks[i] = new Object();
        }
        //初始化哲學(xué)家對(duì)象,并啟動(dòng)線程.
        for (int i=0;i<chopsticks.length;i++){
            Object leftChopsticks = chopsticks[i];
            Object rightChopsticks = chopsticks[(i+1)%chopsticks.length];
            philosophers[i] = new Philosophers(leftChopsticks, rightChopsticks);
            new Thread(philosophers[i],"哲學(xué)家"+i+"號(hào)").start();
        }
    }

}
image.png

同樣發(fā)生死鎖,這里大家陷入了一種循環(huán)等待的狀態(tài),0號(hào)獲取了他左邊的0號(hào)筷子,請(qǐng)求他右邊的1號(hào)筷子,1號(hào)獲取了左邊的1號(hào)筷子,請(qǐng)求等待他右邊的2號(hào)筷子...........5號(hào)獲取了他左邊的5號(hào)筷子等待他右手邊的0號(hào)筷子........這樣就形成了一個(gè)環(huán).

哲學(xué)家問題解決策略
  • 服務(wù)員檢查
    哲學(xué)家拿叉子時(shí) 服務(wù)員檢查叉子數(shù)量
  • 改變一個(gè)哲學(xué)家拿叉子的順序
  • 餐票
    哲學(xué)家就餐時(shí)先拿到餐票就能就餐
  • 領(lǐng)導(dǎo)調(diào)節(jié)(檢測(cè)與恢復(fù)策略)
這里演示改變一個(gè)哲學(xué)家拿叉子的順序修復(fù)
image.png

image.png

這樣就成功的避免了死鎖

4.2 檢測(cè)與恢復(fù)策略:一段時(shí)間檢測(cè)是否有死鎖,如果有就剝奪某個(gè)資源,來解除死鎖

允許死鎖的發(fā)生,但是發(fā)生死鎖后要記錄下來并通過停止線程或其他方式停止死鎖

死鎖檢測(cè)算法
  • 允許發(fā)生死鎖
  • 每次調(diào)用鎖的記錄
  • 定期檢查"鎖的調(diào)用鏈路圖"中是否存在環(huán)路
  • 一旦發(fā)生死鎖,就用死鎖恢復(fù)機(jī)制進(jìn)行恢復(fù)機(jī)制進(jìn)行恢復(fù)
鎖的調(diào)用鏈路圖
恢復(fù)方法1:進(jìn)程終止
  • 逐個(gè)終止線程,直到死鎖消除。
  • 終止順序:
    1. 優(yōu)先級(jí)(重要性,是前臺(tái)交互還是后臺(tái)處理)
    2. 已占用資源、還需要的資源
    3. 已經(jīng)運(yùn)行時(shí)間
恢復(fù)方法2:資源搶占
  • 把已經(jīng)分發(fā)出去的鎖給收回來
  • 讓線程回退幾步,這樣就不用結(jié)束整個(gè)線程,成本比較低
    比如 讓哲學(xué)家把拿起的筷子再放下
  • 缺點(diǎn):可能同一個(gè)線程一直被搶占,那就造成饑餓
4.3 鴕鳥策略

死鎖發(fā)生的幾率特別小,忽略他,等死鎖發(fā)生了,再去處理修改

5 實(shí)際工程中如何避免死鎖

1. 設(shè)置超時(shí)時(shí)間
  • Lock的tryLock(long timeout,TimeUnit unit)
    造成超時(shí)的可能性很多,發(fā)生了死鎖,線程陷入了死循環(huán),線程執(zhí)行很慢.
/**
 * 描述:     用tryLock來避免死鎖
 */
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();
                }
            }
        }
    }
}
線程1獲取到了鎖1
線程2獲取到了鎖2
線程1嘗試獲取鎖2失敗,已重試
線程2獲取到了鎖1
線程2成功獲取到了兩把鎖
線程1獲取到了鎖1
線程1獲取到了鎖2
線程1成功獲取到了兩把鎖
2.多使用并發(fā)類而不是自己設(shè)計(jì)鎖

如ConcurrentHashMap
java.util.concurrent.atomic.

3.盡量降低鎖的使用粒度:用不同的鎖而不是一個(gè)鎖

縮小鎖的臨界區(qū)

4.如果能使用同步代碼塊,就不使用同步方法:自己指定鎖對(duì)象

縮小了同步范圍,可以自己指定鎖對(duì)象

5.給線程指定有意義的名字,方便后期debug和排查
6.避免鎖的嵌套:MustDeadLock 演示的嵌套
7.分配資源前先看能不能收回來:銀行家算法

銀行家算法(Java實(shí)現(xiàn))

8.盡量不要幾個(gè)功能用通一把鎖:專鎖專用

特別感謝:

悟空

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容