死锁——银行转账问题

【死锁】这一篇文章中,我们学习了死锁相关的理论知识,本篇文章来看看死锁案例——银行转账问题,以银行转账问题来讨论死锁、死锁的定位、死锁的修复。

相互转账要点分析

  • 需要两把锁
  • 获取两把锁成功,且余额大于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并发核心知识体系精讲》

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