当前位置 博文首页 > 文章内容

    死锁

    作者: 栏目:未分类 时间:2020-07-08 14:09:07

    本站于2023年9月4日。收到“大连君*****咨询有限公司”通知
    说我们IIS7站长博客,有一篇博文用了他们的图片。
    要求我们给他们一张图片6000元。要不然法院告我们

    为避免不必要的麻烦,IIS7站长博客,全站内容图片下架、并积极应诉
    博文内容全部不再显示,请需要相关资讯的站长朋友到必应搜索。谢谢!

    另祝:版权碰瓷诈骗团伙,早日弃暗投明。

    相关新闻:借版权之名、行诈骗之实,周某因犯诈骗罪被判处有期徒刑十一年六个月

    叹!百花齐放的时代,渐行渐远!



    一、死锁讲解

    在Java中使用多线程,就会有可能导致死锁问题。死锁会让程序一直卡住,不再程序往下执行。我们只能通过中止并重启的方式来让程序重新执行。

    • 这是我们非常不愿意看到的一种现象,我们要尽可能避免死锁的情况发生!

    造成死锁的原因可以概括成三句话:

    • 当前线程拥有其他线程需要的资源
    • 当前线程等待其他线程已拥有的资源
    • 都不放弃自己拥有的资源

    1.1锁顺序死锁

    首先我们来看一下最简单的死锁(锁顺序死锁)是怎么样发生的:

    public class LeftRightDeadlock {
        private final Object left = new Object();
        private final Object right = new Object();
    
        public void leftRight() {
    		// 得到left锁
            synchronized (left) {
    			// 得到right锁
                synchronized (right) {
                    doSomething();
                }
            }
        }
    
        public void rightLeft() {
    		// 得到right锁
            synchronized (right) {
    			// 得到left锁
                synchronized (left) {
                    doSomethingElse();
                }
            }
        }
    }
    

      

    我们的线程是交错执行的,那么就很有可能出现以下的情况:

    • 线程A调用leftRight()方法,得到left锁
    • 同时线程B调用rightLeft()方法,得到right锁
    • 线程A和线程B都继续执行,此时线程A需要right锁才能继续往下执行。此时线程B需要left锁才能继续往下执行。
    • 但是:线程A的left锁并没有释放,线程B的right锁也没有释放。
    • 所以他们都只能等待,而这种等待是无期限的-->永久等待-->死锁

    1.2动态锁顺序死锁

     

     // 转账
        public static void transferMoney(Account fromAccount,
                                         Account toAccount,
                                         DollarAmount amount)
                throws InsufficientFundsException {
    
            // 锁定汇账账户
            synchronized (fromAccount) {
                // 锁定来账账户
                synchronized (toAccount) {
    
                    // 判余额是否大于0
                    if (fromAccount.getBalance().compareTo(amount) < 0) {
                        throw new InsufficientFundsException();
                    } else {
    
                        // 汇账账户减钱
                        fromAccount.debit(amount);
    
                        // 来账账户增钱
                        toAccount.credit(amount);
                    }
                }
            }
        }
    

      

    上面的代码看起来是没有问题的:锁定两个账户来判断余额是否充足才进行转账!

    但是,同样有可能会发生死锁:

    • 如果两个线程同时调用transferMoney()
    • 线程A从X账户向Y账户转账
    • 线程B从账户Y向账户X转账
    • 那么就会发生死锁。

    A:transferMoney(myAccount,yourAccount,10);
    B:transferMoney(yourAccount,myAccount,20);

    1.3协作对象之间发生死锁

    隐式获取两个锁(对象之间协作)

    这种方式也很容易就造成死锁

    二、避免死锁的方法

    避免死锁可以概括成三种方法:

    • 固定加锁的顺序(针对锁顺序死锁)
    • 开放调用(针对对象之间协作造成的死锁)
    • 使用定时锁-->tryLock()

        – 如果等待获取锁时间超时,则抛出异常而不是一直等待!

    2.1固定锁顺序避免死锁

    上面transferMoney()发生死锁的原因是因为加锁顺序不一致而出现的~

    • 正如书上所说的:如果所有线程以固定的顺序来获得锁,那么程序中就不会出现锁顺序死锁问题!

    那么上面的例子我们就可以改造成这样子:

    public class InduceLockOrder {
    
        // 额外的锁、避免两个对象hash值相等的情况(即使很少)
        private static final Object tieLock = new Object();
    
        public void transferMoney(final Account fromAcct,
                                  final Account toAcct,
                                  final DollarAmount amount)
                throws InsufficientFundsException {
            class Helper {
                public void transfer() throws InsufficientFundsException {
                    if (fromAcct.getBalance().compareTo(amount) < 0)
                        throw new InsufficientFundsException();
                    else {
                        fromAcct.debit(amount);
                        toAcct.credit(amount);
                    }
                }
            }
            // 得到锁的hash值
            int fromHash = System.identityHashCode(fromAcct);
            int toHash = System.identityHashCode(toAcct);
    
            // 根据hash值来上锁
            if (fromHash < toHash) {
                synchronized (fromAcct) {
                    synchronized (toAcct) {
                        new Helper().transfer();
                    }
                }
    
            } else if (fromHash > toHash) {// 根据hash值来上锁
                synchronized (toAcct) {
                    synchronized (fromAcct) {
                        new Helper().transfer();
                    }
                }
            } else {// 额外的锁、避免两个对象hash值相等的情况(即使很少)
                synchronized (tieLock) {
                    synchronized (fromAcct) {
                        synchronized (toAcct) {
                            new Helper().transfer();
                        }
                    }
                }
            }
        }
    }
    

     

    得到对应的hash值来固定加锁的顺序,这样我们就不会发生死锁的问题了!

    2.2开放调用避免死锁

    在协作对象之间发生死锁的例子中,主要是因为在调用某个方法时就需要持有锁,并且在方法内部也调用了其他带锁的方法!

    • 如果在调用某个方法时不需要持有锁,那么这种调用被称为开放调用!
    • 同步代码块最好仅被用于保护那些涉及共享状态的操作!

    使用开放调用是非常好的一种方式,应该尽量使用它

    2.3使用定时锁

    使用显式Lock锁,在获取锁时使用tryLock()方法。当等待超过时限的时候,tryLock()不会一直等待,而是返回错误信息。

    使用tryLock()能够有效避免死锁问题

    2.4死锁检测

    虽然造成死锁的原因是因为我们设计得不够好,但是可能写代码的时候不知道哪里发生了死锁。

    JDK提供了两种方式来给我们检测:

    • JconsoleJDK自带的图形化界面工具,使用JDK给我们的的工具JConsole
    • Jstack是JDK自带的命令行工具,主要用于线程Dump分析。

    三、死锁总结

    发生死锁的原因主要由于:

    • 线程之间交错执行

    – 解决:以固定的顺序加锁

    • 执行某方法时就需要持有锁,且不释放

    – 解决:缩减同步代码块范围,最好仅操作共享变量时才加锁

    • 永久等待

    – 解决:使用tryLock()定时锁,超过时限则返回错误信息