由于不当的执行顺序导致的死锁

Posted Gerald Newton

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了由于不当的执行顺序导致的死锁相关的知识,希望对你有一定的参考价值。

本文将会讨论一下顺序死锁的问题。

我们来讨论一个经常存在的账户转账的问题。账户A要转账给账户B。为了保证在转账的过程中A和B不被其他的线程意外的操作,我们需要给A和B加锁,然后再进行转账操作, 我们看下转账的代码:

public void transferMoneyDeadLock(Account from,Account to, int amount) throws InsufficientAmountException 
    synchronized (from)
        synchronized (to)
            transfer(from,to,amount);
        
    


private void transfer(Account from,Account to, int amount) throws InsufficientAmountException 
    if(from.getBalance() < amount)
        throw new InsufficientAmountException();
    else
        from.debit(amount);
        to.credit(amount);
    

看起来上面的程序好像没有问题,因为我们给from和to都加了锁,程序应该可以很完美的按照我们的要求来执行。

那如果我们考虑下面的一个场景:

A:transferMoneyDeadLock(accountA, accountB, 20)
B:transferMoneyDeadLock(accountB, accountA, 10)

如果A和B同时执行,则可能会产生A获得了accountA的锁,而B获得了accountB的锁。从而后面的代码无法继续执行,从而导致了死锁。

对于这样的情况,我们有没有什么好办法来处理呢?

加入不管参数怎么传递,我们都先lock accountA再lock accountB是不是就不会出现死锁的问题了呢?

我们看下代码实现:

private void transfer(Account from,Account to, int amount) throws InsufficientAmountException 
    if(from.getBalance() < amount)
        throw new InsufficientAmountException();
    else
        from.debit(amount);
        to.credit(amount);
    


public void transferMoney(Account from,Account to, int amount) throws InsufficientAmountException 

   int fromHash= System.identityHashCode(from);
   int toHash = System.identityHashCode(to);

   if(fromHash < toHash)
       synchronized (from)
           synchronized (to)
               transfer(from,to, amount);
           
       
   else if(fromHash < toHash)
        synchronized (to)
            synchronized (from)
                transfer(from,to, amount);
            
        
    else
       synchronized (lock)
       synchronized (from) 
           synchronized (to) 
               transfer(from, to, amount);
           
         
       
   

上面的例子中,我们使用了System.identityHashCode来获得两个账号的hash值,通过比较hash值的大小来选定lock的顺序。

如果两个账号的hash值恰好相等的情况下,我们引入了一个新的外部lock,从而保证同一时间只有一个线程能够运行内部的方法,从而保证了任务的执行而不产生死锁。

在此我向大家推荐一个架构学习交流圈。交流学习微信:539413949(里面有大量的面试题及答案)里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

避免死锁的银行家算法

多线程操作系统在进程调度(资源分配)的时候可能会发生死锁。

引起死锁的直接原因是竞争不可抢占的互斥资源。这种资源有可能是临界资源,例如打印机;也有可能是可消耗性资源,例如信号量。

引起死锁的间接原因进程推进顺序不当。即系统单独运行进程P1或者P2都没有问题,但是调度两个进程同时进行时,由于调度顺序导致两个进程竞争资源产生死锁。

-----------------------------

产生死锁的四个必要条件:

1、互斥条件。进程已经分配到的资源具有互斥性,不能同时被其他进程共享。

2、请求和保持条件。进程因为请求资源而被阻塞时仍然占有已经分配给自己的资源。

3、不可抢占条件。进程已经分配到的资源不可以被抢占,只能等进程执行完以后自己释放。

4、循环等待条件。每个进程都在等待其他进程所占有的不可抢占的互斥资源。

---------------------------------

预防死锁的协议--这几个协议都有各自的局限性,并不适用

1、破坏“请求保持条件”的第一种协议。协议规定进程在开始运行时必须一次性申请全部所需的资源。改进后,协议规定进程可以只分配当前所需资源,但是只有已分配资源全部用完并释放后才可以再次申请资源。这种改进就是把一个进程按阶段当做多个“小进程”来对待,仍然具有第一种协议的缺点,比第一种情况稍好一些。

缺点:资源严重浪费,且容易引起进程饥饿现象(即进程长时间得不到运行的机会,需要资源越多的进程越容易出现此现象);

2、破坏“不可抢占”的协议。协议规定已经持有不可抢占资源的进程在申请新资源失败后,必须释放已经持有的资源。

缺点:该方法实现复杂,且代价大。主要是因为,这种情况下进程释放某些资源时候难以保存资源的现场信息,相当于之前的工作白做了。

问题(这个不止是缺点):进程被无限推迟。一个进程申请了资源,又被迫释放资源,又申请资源,又被迫释放资源,如果这种资源恰好是现场信息不能保存的那种,那么进程执行了这么久还跟没执行一个样子。

3、破坏“循环等待条件”的协议:规定系统中的资源必须排序,且每个进程按资源序号递增的顺序申请资源。

缺点:资源的排序难以确定,增加新设备困难。

-----------------------------

目前使用的避免死锁的方法:银行家算法

先看两个定义:

不安全状态:没有办法定义什么是不安全状态,安全状态之外的全部是不安全状态。

安全状态:存在一个进程的安全安全序列,就属于安全状态。

这两个概念的实际内容跟他们的名字并不相符。准确地说,安全状态是全部安全的状态的一个子集,这个子集中的每个状态都可以用至少一个特例来证明自身状态是安全的。反之,不安全状态中也包含了本身可能安全,但是不能直接证明的状态。

 

银行家算法:

1、如果进程的Request《=进程的Need,转向步骤2;否则认为出错

2、如果进程的Request《=系统现有的Avaliable,转向步骤3;否则进程等待

3、推演此次资源分配后的系统状态

4、执行安全性算法,如果推演后的系统状态仍是处于安全状态,则执行此次分配;否则,不执行此次分配

银行家算法中最关键步骤是如何利用安全性算法判断安全状态。

教科书上介绍的安全性算法是一个个遍历看能不能找到安全序列。网上搜到的代码的实现方式都是教科书式的Demo,采用循环遍历来实现。这种方式明显不可取,即便是一台个人PC也有数十个进程同时运行,用循环来做安全性算法难道合适吗?

以上是关于由于不当的执行顺序导致的死锁的主要内容,如果未能解决你的问题,请参考以下文章

避免死锁的银行家算法

并发问题

线程池使用不当也会死锁?

死锁产生的原因及条件和手写死锁

转:java高并发学习记录-死锁,活锁,饥饿

Java死锁及死锁的避免