一起养成写作习惯!这是我参与「日新计划 4 月更文挑战」的第19天,点击查看活动详情

本系列专栏Java并发编程专栏 – 元浩875的专栏 – ()

前言

上一篇文章我们说了解决原子性问题,我们使用互斥锁来保证共享变量同一时刻只有一个线程可以操作,然后说了编译器帮我实现加解锁的synchronized关键字,以及最重要的锁和其保护的资源之间的关系是1:N,那本章就重点说一下这个如何一把锁保护多个资源。

正文

其实这个问题是个很复杂的问题,我们从简单的来看。

保护多个没有关联的资源

假如多个资源没有关联关系,而对不同资源的操作是不需要互斥的,我们就可以创建多个锁来保护不同的资源,这种对受保护对象进行细化管理,能够提升性能,这种锁叫做细粒度锁

比如现在有个账户类Account,里面有2个成员变量,分别是余额balance和密码password,取款withdraw()和查看账户余额getBalance()会访问余额balance,我们创建一个balLock锁来保护balance这个资源;而更改密码updatePassword()和查看密码getPassword()会修改账户密码password,我们创建一个pwLock锁来保护password资源。

class Account {
  // 锁:保护账户余额
  private final Object balLock
    = new Object();
  // 账户余额  
  private Integer balance;
  // 锁:保护账户密码
  private final Object pwLock
    = new Object();
  // 账户密码
  private String password;
  // 取款
  void withdraw(Integer amt) {
    synchronized(balLock) {
      if (this.balance > amt){
        this.balance -= amt;
      }
    }
  } 
  // 查看余额
  Integer getBalance() {
    synchronized(balLock) {
      return balance;
    }
  }
  // 更改密码
  void updatePassword(String pw){
    synchronized(pwLock) {
      this.password = pw;
    }
  } 
  // 查看密码
  String getPassword() {
    synchronized(pwLock) {
      return password;
    }
  }
}

当然这里我们可以使用同一把互斥锁来保护多个资源,比如用this这一把锁,但是就会导致取款、查看余额和修改密码这个不相关的操作是串行的,大大降低了性能。

保护有关联的多个资源

这个就比较复杂了,假如银行有个转账业务,账户A减少100,账户B多100,这2个账户是有关联关系的,那这种如何解决呢

首先创建一个Account类,该类有一个成员变量余额:balance,还有一个用于转账的方法:transfer(),代码如下:

class Account {
  private int balance;
  // 转账
  void transfer(
      Account target, int amt){
    if (this.balance > amt) {
      this.balance -= amt;
      target.balance += amt;
    }
  } 
}

这时如果按照前面了解的知识,防止当前账户的转账方法同时被多个线程调用,直接使用锁把这个方法锁起来,这里直接用synchronized修饰该方法:

class Account {
  private int balance;
  // 转账
  synchronized void transfer(
      Account target, int amt){
    if (this.balance > amt) {
      this.balance -= amt;
      target.balance += amt;
    }
  } 
}

这个代码中,我们用当前对象this做为锁,但是临界区中却有2个资源,分别转出账户余额:this.balance和转入账户余额:target.balance,也就是我们用一把锁来保护多个资源,这真的可靠吗

其实不可靠,this这把锁只能保护自己的余额,却无法保护别人的余额,示意图如下:

Java并发编程 | 如何使用一把锁保护多个资源

我们来举个例子,假设有A、B、C三个账户,余额都是200,现在有2个线程分别执行转账操作:A转账给B 100,B转账给C 100,最后期望结果是A余额100,B余额200,C余额300。

假设线程1执行A给B转账,线程2执行B给C转账,这2个线程分别在2颗CPU上执行,线程1锁定的对象是A.this,线程2锁定的对象是B.this,这2个线程同时可以进入临界区,这时就有问题了,线程1和线程2都能读取到B的值是200,但是在执行完方法后,B的值可能是100,可能是300。

当线程1后于线程2写B.balance,线程2写的B.balance值就被线程1覆盖,也就是300;当线程2后于线程1写B.balance,线程1写的B.balance值被线程2覆盖,也就是100。

这里出现问题的原因就是锁所保护的资源不对。

使用锁的正确方法

解决上面问题的方法我们可以使用覆盖一个全部资源的锁,在前面的例子中,this是对象级别的锁,所以A对象和B对象都有自己的锁,如何使用一个共同的锁呢 我们可以使用Account.class作为共享锁,Account.class是所有Account对象共享的,是唯一的,代码如下:

class Account {
  private int balance;
  // 转账
  void transfer(Account target, int amt){
    synchronized(Account.class) {
      if (this.balance > amt) {
        this.balance -= amt;
        target.balance += amt;
      }
    }
  } 
}

然后这里就可以一个锁保护了多个受保护资源,示意图如下:

Java并发编程 | 如何使用一把锁保护多个资源

但是这里也有个很明显的问题,比如A给B转账,C给D转账,这本来是2个不相关的操作,但是由于使用了Accout.class作为锁,就会导致这2个操作是阻塞的,性能很差

总结

本章说了一把锁保护多个资源的情况,当资源之间没有联系,可以细化锁;当多个资源有联系时,使用锁就需要额外注意了。上面最后的例子也存在严重的性能问题,具体如何优化,我们后面文章再讨论。