通过银行转账问题解说死锁解决方案

大家都知道,在并发情况下对两个账户进行转账操作可能会产生死锁,可能出现死锁的原因是,并发情况下对两个账户的操作无法保证其执行顺序。

  1. 并发问题描述
    假如现在执行下面的操:

线程一执行的是:【账户A】给【账户B】转账

线程二执行的是:【账户B】给【账户A】转账

如果两个转账动作同时执行,则会出现线程一会请求对【账户B】进行加锁,线程二会请求对【账户A】进行加锁

由于此时的【账户A】已由线程一进行锁定,【账户B】已由线程二进行锁定 此时就会产生死锁问题。接下来分析一下产生死锁的原因,以及如何避免死锁。

  1. 如何避免死锁
    有个叫 Coffman 的牛人总结过一条经验,只有当以下四个条件同时发生,才会出现死锁,所以只要打破其中一个条件,就可以避免死锁:
  • 互斥,共享资源 X 和 Y 只能被一个线程占用
  • 占有且等待,线程 A 获取到资源 X,在等待资源 Y 的时候,不释放资源 X
  • 不可抢占,其他线程不能强行抢占线程 A 占有的资源
  • 循环等待,线程 A 等待线程 B 占有的资源,线程 B 等待线程 A 的资源
    首先,互斥这个条件是没法破坏的,因为锁存在的目的就是互斥,对于剩下的三个条件都可破坏。

2.1 破坏占有且等待

对于占有且等待,可以同时获取要使用的多个资源锁X和Y,这样就不会存在取得了X还要等待Y。这种方式只在需要获取的资源锁较少的情况下使用,如果要获取的资源锁很多(例如10个),就不太可行。代码实现时,我们通过增加一个 Allocator 账号管理员对象,并且将其设置为单例,每次进行转账的时候,我们都先通过 Allocator 分配账号,如果分配账号成功,则进行转账,如果失败则重新获取,可以设置一个失败次数或是超时时间,达到失败次数或超时时间则转账失败。如下是代码实现,

public class Allocator {

private static class InstanceHolder {
    static Allocator instance = new Allocator();
}

public static Allocator getInstance() {
    return InstanceHolder.instance;
}

/**

 * 上锁的账户列表
   */
   private List<Account> lockAccountList = new ArrayList<>();

/**

 * 申请分配账户
   *
 * @param from 从这个账户转钱
 * @param to   转钱到这个账户
   */
   public synchronized void apply(Account from, Account to) {
   while (lockAccountList.contains(from) || lockAccountList.contains(to)) {
       // 如果两个账户中,只要有一个账户上锁了,则申请失败,进入循环等待
       try {
           // 阻塞当前线程,等待通知
           this.wait();
       } catch (InterruptedException e) {
       }
   }
   lockAccountList.add(from);
   lockAccountList.add(to);
   }

/**

 * 释放账户锁
   *
 * @param from 从这个账户转钱
 * @param to   转钱到这个账户
   */
   public synchronized void free(Account from, Account to) {
   lockAccountList.remove(from);
   lockAccountList.remove(to);
   // 通知所有线程,让其再次获取锁
   this.notifyAll();
   }

private Allocator() {
}
}

2.2 破坏不可抢占条件

对于不可抢占,可以获取了部分资源,再进一步获取其他资源时如果获取不到时,把已经获取的资源一起释放掉。破坏不可抢占条件,需要线程在获取不到锁的情况下主动释放它拥有的资源。当我们使用synchronized的时候,线程是没有办法主动释放它占有的资源的。因为,synchronized在申请不到资源时,会使线程直接进入阻塞状态,而线程进入了阻塞状态就不能主动释放占有的资源。java.util.concurrent中的Lock接口,提供了如下三种设计思想都可以解决死锁的不可抢占条件:

  • 能够响应中断:线程处于阻塞状态时可以接收中断信号。我们便可以给阻塞的线程发送中断信号,唤醒线程,线程便有机会释放它曾经拥有的锁。这样便可破坏不可抢占条件。
  • 支持超时:如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个错误,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
  • 非阻塞地获取锁:如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也有机会释放曾经持有的锁。这样也可以破坏不可抢占条件。
    对应方法
// 支持中断的 API
void lockInterruptibly() throws InterruptedException;

// 支持超时的 API
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

// 支持非阻塞获取锁的 API
boolean tryLock();

使用非阻塞的获取锁实现:

public class Preemptible {
public boolean transferMoney(Account fromAcct, Account toAcct) {
    while (true) {
        // 使用tryLock()获取锁
        if (fromAcct.lock.tryLock()) {
            try {
                // 使用tryLock()获取锁
                if (toAcct.lock.tryLock()) {
                    return true;
                }
            } finally {
                // 释放前面获取到的锁
                fromAcct.lock.unlock();
            }
        }
    }
}
}

2.3 破坏循环等待条件

对于循环等待,可以将需要获取的锁资源排序,按照顺序获取,这样就不会多个线程交叉获取相同的资源导致死锁,而是在获取相同的资源时就等待,直到它释放。比如根据账号的主键 id 进行排序,从小到大的获取锁,这样就可以避免循环等待。

账号排序:

public class Account {

private Integer id;

private int balance;

/**

 * 转账
   *
 * @param target 目标
 * @param amt    金额
   */
   void transfer(Account target, int amt) {
   Account left = this;
   Account right = target;
   if (this.id > target.id) {
       left = target;
       right = this;
   }
   // 锁定序号小的账户
   synchronized (left) {
       // 锁定序号大的账户
       synchronized (right) {
           if (this.balance > amt) {
               this.balance -= amt;
               target.balance += amt;
           }
       }
   }
   }
   }
  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值