1 如何解决银行转账时的并发问题

当出现 A 将钱转给 B 的时候,什么样的方案可以避免在转账时没有并发问题。先看以下代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Account {
    /*
     * 账户余额
     */
    private int balance;

    /*
     * 转账
     * 
     * @param target 转入账户
     * @param amt    转入数额
     */
    void transfer(Account target, int amt) {
        // 锁定转出账户
        synchronized(this) {
            // 锁定转入账户
            synchronized(target) {
                if (this.balance > amt) {
                    this.balance -= amt;
                    target.balance += amt;
                }
            }
        }
    }
}

如上代码虽然能够实现转入转出,但是却存在并发问题,如果出现 A 给 B 转账,同时 B 给 A 转账,则可能出现双方均卡在了 synchronized(this) 这一步操作上,因为双方均将自己加锁,但获取对方的锁的时候,发现已经加锁,于是一直等待,就出现了死锁。

2 如何避免死锁

有个叫 Coffman 的牛人总结过一条经验,只有当以下四个条件同时发生,才会出现死锁,所以只要打破其中一个条件,就可以避免死锁:

  1. 互斥,共享资源 X 和 Y 只能被一个线程占用
  2. 占有且等待,线程 A 获取到资源 X,在等待资源 Y 的时候,不释放资源 X
  3. 不可抢占,其他线程不能强行抢占线程 A 占有的资源
  4. 循环等待,线程 A 等待线程 B 占有的资源,线程 B 等待线程 A 的资源

首先,互斥这个条件是没法破坏的,因为锁存在的目的就是互斥,对于剩下的三个条件都可破坏

2.1 破坏占有且等待

对于上述案例,这两个账户,我们可以采用同时占用,同时释放的方式,这样就不会出现抢占一个资源等待另一个资源的情况。代码实现时,我们通过增加一个 Allocator 账号管理员对象,并且将其设置为单例,每次进行转账的时候,我们都先通过 Allocator 分配账号,如果分配账号成功,则进行转账,如果失败则重新获取,可以设置一个失败次数或是超时时间,达到失败次数或超时时间则转账失败。如下是代码实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
 * 单例模式实现的账号管理员
 */
class Allocator {
    /**
     * 上锁的账户列表
     */
    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) {
              }
         } else {
             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();
    }
}

2.2 破坏不可抢占条件

java.util.concurrent 这个包下面提供的 Lock 是可以轻松解决这个问题的 // todo 待完成

2.3 破坏循环等待条件

通过对锁进行排序,按照特定顺序获取的方式避免循环等待。比如通过如下 1~6 代码,根据账号的主键 id 进行排序,从小到大的获取锁,这样就可以避免循环等待。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Account {
  private int id;
  private int balance;
  // 转账
  void transfer(Account target, int amt){
    Account left = this        // 1
    Account right = target;    // 2
    if (this.id > target.id) { // 3
      left = target;           // 4
      right = this;            // 5
    }                          // 6
    // 锁定序号小的账户
    synchronized(left){
      // 锁定序号大的账户
      synchronized(right){ 
        if (this.balance > amt){
          this.balance -= amt;
          target.balance += amt;
        }
      }
    }
  } 
}