美文网首页
Java死锁和解决办法(2)

Java死锁和解决办法(2)

作者: Bfmall | 来源:发表于2023-04-18 13:23 被阅读0次

接着上篇:https://www.jianshu.com/p/524aac6ff958

3.2 动态锁顺序死锁

3.2.1 动态锁顺序死锁的产生与示例

动态锁顺序死锁与上面的锁顺序死锁其实最本质的区别,就在于动态锁顺序死锁锁住的资源无法确定或者会发生改变。 比如说银行转账业务中,账户A向账户B转账,账户B也可以向账户A转账,这种情况下如果加锁的方式不正确就会发生死锁。
比如如下代码: 定义简单的账户类Account

public class Account {
    /** 账户 */
    public String number;
    /** 余额 */
    public BigDecimal balance;

    public Account(String number, BigDecimal balance) {
        this.number = number;
        this.balance = balance;
    }

    public void setNumber(String number) {
        this.number = number;
    }

    public void setBalance(BigDecimal balance) {
        this.balance = balance;
    }
}

定义转账类TransferMoney,其中有transferMoney01()方法用于accountFrom账户向accountTo转账金额amt:

public class TransferMoney {
    private static final String TAG = TransferMoney.class.getName();

    /**
     * 转账方法
     *
     * @param accountFrom       转账方
     * @param accountTo         接收方
     * @param amt               转账金额
     * @throws Exception
     */
    public static void transferMoney01(Account accountFrom,
                                     Account accountTo,
                                     BigDecimal amt) throws Exception {

        synchronized (accountFrom) {
            synchronized (accountTo) {
                BigDecimal formBalance = accountFrom.balance;
                if (formBalance.compareTo(amt) < 0) {
                    throw new Exception(accountFrom.number + " balance is not enough.");
                } else {
                    //减去
                    accountFrom.setBalance(formBalance.subtract(amt));
                    //加上
                    accountTo.setBalance(accountTo.balance.add(amt));
                    Log.i(TAG, "Form" + accountFrom.number + ": " + accountFrom.balance.toPlainString()
                            + "\t" + "To" + accountTo.number + ": " + accountTo.balance.toPlainString());
                }
            }
        }
    }
}

上面这个类看似规定了锁的顺序由accountFrom到accountTo不会产生死锁,但是这个accountFrom和accountTo是由调用方来传入的,当A向B转账时accountFrom = A,accountTo = B;当B向A转账时accountFrom = B,accountTo = A;假设两者在同一时刻给对方发起转账,则仍然存在3.1中锁顺序死锁问题。
比如如下测试:

public void testDeadLock01() {
        // 账户A && 账户B
        Account accountA = new Account("111111", new BigDecimal(1000));
        Account accountB = new Account("2222222", new BigDecimal(1000));
        // 循环创建线程 A -> B ; B -> A 各10个线程
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                try {
                    // 转账顺序 A -> B
                    TransferMoney.transferMoney01(accountA, accountB, new BigDecimal(10));
                } catch (Exception e) {
                    return;
                }
            }).start();

            new Thread(() -> {
                try {
                    // 转账顺序 B -> A
                    TransferMoney.transferMoney01(accountB, accountA, new BigDecimal(10));
                } catch (Exception e) {
                    return;
                }
            }).start();
        }
    }

程序执行无法正确结束,如下所示:

Form2222222: 990    To111111: 1010
Form111111: 1000    To2222222: 1000
Form111111: 990 To2222222: 1010
Form111111: 980 To2222222: 1020
Form2222222: 1010   To111111: 990

线程未执行完毕,产生死锁。

3.2.2 解决方案:

解决动态锁顺序死锁的办法,就是通过一定的手段来严格控制加锁的顺序。比如通过对象中某一个唯一的属性值比如id;或者也可以通过对象的散列值+hash冲突解决来控制加锁的顺序。 我们通过对象的散列值+hash冲突解决的方式来优化上面的代码:

public class TransferMoney {
    private static final String TAG = TransferMoney.class.getName();

    /** hash 冲突时使用第三个锁(优秀的hash算法冲突是很少的!) */
    private static final Object conflictShareLock = new Object();

    /**
     * 转账方法
     *
     * @param accountFrom       转账方
     * @param accountTo         接收方
     * @param amt               转账金额
     * @throws Exception
     */
    public static void transferMoney02(Account accountFrom,
                                     Account accountTo,
                                     BigDecimal amt) throws Exception {
        // 计算hash值
        int accountFromHash = System.identityHashCode(accountFrom);
        int accountToHash = System.identityHashCode(accountTo);
        Log.i(TAG, "transferMoney02==>accountFromHash="+accountFromHash+", accountToHash="+accountToHash);
        // 如下三个分支能一定控制账户之间的转是不会产生死锁的
        if (accountFromHash > accountToHash) {
            synchronized (accountFrom) {
                synchronized (accountTo) {
                    transferMoneyHandler(accountFrom, accountTo, amt);
                }
            }
        } else if (accountToHash > accountFromHash) {
            synchronized (accountTo) {
                synchronized (accountFrom) {
                    transferMoneyHandler(accountFrom, accountTo, amt);
                }
            }
        } else {
            // 解决hash冲突
            synchronized (conflictShareLock) {
                synchronized (accountFrom) {
                    synchronized (accountTo) {
                        transferMoneyHandler(accountFrom, accountTo, amt);
                    }
                }
            }
        }

    }

    /**
     * 账户金额增加处理
     *
     * @param accountFrom       转账方
     * @param accountTo         接收方
     * @param amt               转账金额
     * @throws Exception
     */
    private static void transferMoneyHandler(Account accountFrom,
                                             Account accountTo,
                                             BigDecimal amt) throws Exception {
        if (accountFrom.balance.compareTo(amt) < 0) {
            throw new Exception(accountFrom.number + " balance is not enough.");
        } else {
            accountFrom.setBalance(accountFrom.balance.subtract(amt));
            accountTo.setBalance(accountTo.balance.add(amt));
            Log.i(TAG, "Form" + accountFrom.number + ": " + accountFrom.balance.toPlainString()
                    +"\t" + "To" +  accountTo.number + ": " + accountTo.balance.toPlainString());
        }
    }
}

测试代码:

public void testDeadLock02() {
        // 账户A && 账户B
        Account accountA = new Account("111111", new BigDecimal(1000));
        Account accountB = new Account("2222222", new BigDecimal(1000));
        // 循环创建线程 A -> B ; B -> A 各10个线程
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    // 转账顺序 A -> B
                    TransferMoney.transferMoney02(accountA, accountB, new BigDecimal(10));
                } catch (Exception e) {
                    return;
                }
            }).start();

            new Thread(() -> {
                try {
                    // 转账顺序 B -> A
                    TransferMoney.transferMoney02(accountB, accountA, new BigDecimal(10));
                } catch (Exception e) {
                    return;
                }
            }).start();
        }
    }

测试结果:

transferMoney02==>accountFromHash=104721827, accountToHash=98044064
Form111111: 990 To2222222: 1010
transferMoney02==>accountFromHash=104721827, accountToHash=98044064
transferMoney02==>accountFromHash=98044064, accountToHash=104721827
Form111111: 980 To2222222: 1020
Form2222222: 1010   To111111: 990
transferMoney02==>accountFromHash=104721827, accountToHash=98044064
Form111111: 980 To2222222: 1020
transferMoney02==>accountFromHash=98044064, accountToHash=104721827
Form2222222: 1010   To111111: 990
transferMoney02==>accountFromHash=104721827, accountToHash=98044064
transferMoney02==>accountFromHash=98044064, accountToHash=104721827
Form111111: 980 To2222222: 1020
transferMoney02==>accountFromHash=98044064, accountToHash=104721827
Form2222222: 1010   To111111: 990
Form2222222: 1000   To111111: 1000
transferMoney02==>accountFromHash=104721827, accountToHash=98044064
Form111111: 990 To2222222: 1010
transferMoney02==>accountFromHash=104721827, accountToHash=98044064
transferMoney02==>accountFromHash=98044064, accountToHash=104721827
Form111111: 980 To2222222: 1020
Form2222222: 1010   To111111: 990transferMoney02==>accountFromHash=98044064, accountToHash=104721827
Form2222222: 1000   To111111: 1000transferMoney02==>accountFromHash=104721827, accountToHash=98044064
transferMoney02==>accountFromHash=98044064, accountToHash=104721827
Form111111: 990 To2222222: 1010
Form2222222: 1000   To111111: 1000
transferMoney02==>accountFromHash=104721827, accountToHash=98044064
Form111111: 990 To2222222: 1010
transferMoney02==>accountFromHash=98044064, accountToHash=104721827
transferMoney02==>accountFromHash=98044064, accountToHash=104721827
Form2222222: 1000   To111111: 1000
transferMoney02==>accountFromHash=104721827, accountToHash=98044064
transferMoney02==>accountFromHash=98044064, accountToHash=104721827
transferMoney02==>accountFromHash=104721827, accountToHash=98044064
Form2222222: 990    To111111: 1010
Form111111: 1000    To2222222: 1000
Form111111: 990 To2222222: 1010
Form2222222: 1000   To111111: 1000

在上面两种死锁的产生原因都是因为两个线程以不同的顺序获取相同的所导致的,而解决的办法都是通过一定的规范来严格控制加锁的顺序,这样就能正确的规避死锁的风险。

参考:
https://blog.csdn.net/chow__zh/article/details/123968286

相关文章

网友评论

      本文标题:Java死锁和解决办法(2)

      本文链接:https://www.haomeiwen.com/subject/yqvhjdtx.html