首页 > 技术文章 > 一不小心就死锁了,怎么办?

YXBLOGXYY 2022-03-10 23:04 原文

1)上一节我们用一把大锁锁住银行的转账业务,这样会造成什么样的问题?

  • 所有账户的转账操作都是串行的,性能太差

  • A 转账户 B、账户 C 转账户 D 这两个转账操作现实世界里是可以并行的,但是在这个方案里却被串行化了

2)那么如何优化可以让我们的账户之间转账和入账能够并行执行呢?

  • 在 transfer() 方法内部,我们首先尝试锁定转出账户 this(先把转出账本拿到手),然后尝试锁定转入账户 target(再把转入账本拿到手),只有当两者都成功时,才执行转账操作。

     

     

 
 class Account {
   private int balance;
   // 转账
   void transfer(Account target, int amt){
     // 锁定转出账户
     synchronized(this) {              
       // 锁定转入账户
       synchronized(target) {          
         if (this.balance > amt) {
           this.balance -= amt;
           target.balance += amt;
        }
      }
    }
  }
 }
  • 优化后,账户 A 转账户 B 和账户 C 转账户 D 这两个转账操作就可以并行了

3)上面我们使用了细粒度锁,那可能会导致什么代价?

  • 死锁

     

     

4)死锁的四个必要条件?

  • 互斥资源

  • 循环等待

  • 请求与保持

  • 不可剥夺

5)那如何避免死锁呢?

  • 对于“占用且等待”这个条件,我们可以一次性申请所有的资源,这样就不存在等待了。

  • 对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。

  • 对于“循环等待”这个条件,可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后自然就不存在循环了。

6)5是理论的,那实际的操作是怎样的?

  • 一次性申请所有资源

    • 增加一个账本管理员,然后只允许账本管理员从文件架上拿账本,当张三要AB的时候,管理员看看架子,只有AB都在才会给张三,否则不会给

       
       class Allocator {
         private List<Object> als =
           new ArrayList<>();
         // 一次性申请所有资源
         synchronized boolean apply(
           Object from, Object to){
           if(als.contains(from) ||
                als.contains(to)){
             return false;  
          } else {
             als.add(from);
             als.add(to);  
          }
           return true;
        }
         // 归还资源
         synchronized void free(
           Object from, Object to){
           als.remove(from);
           als.remove(to);
        }
       }
       
       class Account {
         // actr应该为单例
         private Allocator actr;
         private int balance;
         // 转账
         void transfer(Account target, int amt){
           // 一次性申请转出账户和转入账户,直到成功
           while(!actr.apply(this, target))
             
           try{
             // 锁定转出账户
             synchronized(this){              
               // 锁定转入账户
               synchronized(target){          
                 if (this.balance > amt){
                   this.balance -= amt;
                   target.balance += amt;
                }
              }
            }
          } finally {
             actr.free(this, target)
          }
        }
       }

       

  • 主动释放它占有的资源

    • synchronized 做不到,synchronized 申请资源的时候,如果申请不到,线程直接进入阻塞状态了

    • Java 在语言层次确实没有解决这个问题,不过在 SDK 层面还是解决了的,java.util.concurrent 这个包下面提供的 Lock 是可以轻松解决这个问题的

  • 对资源进行排序,然后按序申请资源

    • 锁排序法

7)当我们在编程世界里遇到问题时,应该怎样思考?

  • 不局限于当下,可以换个思路,向现实世界要答案,利用现实世界的模型来构思解决方案

推荐阅读