死鎖——銀行轉賬問題

【死鎖】這一篇文章中,我們學習了死鎖相關的理論知識,本篇文章來看看死鎖案例——銀行轉賬問題,以銀行轉賬問題來討論死鎖、死鎖的定位、死鎖的修復。

相互轉賬要點分析

  • 需要兩把鎖
  • 獲取兩把鎖成功,且餘額大於0,則扣除轉出人,增加收款人的餘額,而且這是個原子操作
  • 獲取鎖的順序相反導致死鎖

代碼演示

public class TransferMoney implements Runnable {

    private int flag = 1;
    private static Account a = new Account(500);
    private 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;
        //定義兩個線程,flag = 1和0分別模擬a和b
        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) {
            //a轉賬給b
            transferMoney(a, b, 200);
        }
        if (flag == 0) {
            //b轉賬給a
            transferMoney(b, a, 200);
        }
    }

    /**
     * 轉賬方法
     *
     * @param from   轉出人
     * @param to     收款人
     * @param amount 轉賬金額
     */
    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;
    }
}

打印結果:啓動程序,發現並未打印結果輸出,而且程序未停止,這就發生了死鎖。
死鎖

死鎖定位

  • jstack命令行
    未完待續…

  • 利用工具類:ThreadMXBean類,上面死鎖程序的main函數改成如下:

    public static void main(String[] args) throws InterruptedException {
        TransferMoney r1 = new TransferMoney();
        TransferMoney r2 = new TransferMoney();
        r1.flag = 1;
        r2.flag = 0;
        //定義兩個線程,flag = 1和0分別模擬a和b
        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("發現死鎖" + threadInfo.getThreadName());
            }
        }
    }

打印結果:
死鎖檢測

死鎖修復

  • 【死鎖】這篇文章提到多種死鎖修復的方案,這裏用到的是死鎖避免策略:把獲取兩把鎖的規則改一下,原來的規則是先獲取轉出人的鎖,再獲取收款人的鎖,這就會造成兩個轉出人都在等對方釋放鎖的情況。
  • 現在我們把規則改成:所有的交易都先獲取更小的鎖,獲取到了小的鎖才能獲取大鎖,這就避免了環形的死鎖,假如說這兩個鎖的大小一樣,這時候就需要一把額外的鎖來進行交易流程的控制,相當於一場“加時賽”。
public class TransferMoney implements Runnable {

    private int flag = 1;
    private static Account a = new Account(500);
    private static Account b = new Account(500);
    private 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;
        //定義兩個線程,flag = 1和0分別模擬a和b
        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) {
            //a轉賬給b
            transferMoney(a, b, 200);
        }
        if (flag == 0) {
            //b轉賬給a
            transferMoney(b, a, 200);
        }
    }

    /**
     * 轉賬方法
     *
     * @param from   轉出人
     * @param to     收款人
     * @param amount 轉賬金額
     */
    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 + "元");
            }
        }

        //使用類的hash值來幫助排序
        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 {
            //發生hash碰撞時,可以增加鎖來進行控制,類似“加時賽”
            synchronized (lock) {
                synchronized (to) {
                    synchronized (from) {
                        new Helper().transfer();
                    }
                }
            }
        }
    }

    /**
     * 收款賬戶
     */
    static class Account {

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

打印結果:
修復死鎖
筆記來源:慕課網悟空老師視頻《Java併發核心知識體系精講》

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