《Java 并发编程实战》04 阅读笔记

81 阅读3分钟

上节课作者卖了个关子,就是如何用同一把锁保护多个资源。

作者表示,可以用一把互斥锁来保护多个资源,例如用 this 这一把锁来管理账户类里所有的资源:账户余额和用户密码。

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;
    }
  }
}

具体表现为,将示例程序中所有的方法都增加同步关键字 synchronized 即可。
但作者认为,用一把锁有个问题,就是性能太差,会导致取款、查看余额、修改密码、查看密码这四个操作都是串行的。
而用两把锁,取款和修改密码是可以并行的。
这样用不同的锁对受保护资源进行精细化管理,能够提升性能(这种锁也叫细粒度锁)。
所以,作者认为不同的资源最好用不同的锁来保护。

再看一段代码

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

怎么保证转账操作 transfer() 没有并发问题呢?
作者表示,在这段代码中,临界区内有两个资源,分别是转出账户的余额 this.balance 和转入账户的余额 target.balance 并且用的是同一把锁 this 这把锁可以保护自己的余额 this.balance 却保护不了别人的余额 target.balance(详见用锁 this 保护 this.balance 和 target.balance 的示意图)。

再如,假设有 A、B、C 三个账户,余额都是 200 元,用两个线程分别执行两个转账操作:账户 A 转给账户 B 100 元,账户 B 转给账户 C 100 元,最后期望的结果应该是账户 A 的余额是 100 元,账户 B 的余额是 200 元,账户 C 的余额是 300 元(详见分析过程与并发转账示意图)。

作者表示,只要锁能覆盖所有受保护资源就可以了。示例代码如下

class Account {
  private Object lock;
  private int balance;
  private Account();
  // 创建Account时传入同一个lock对象
  public Account(Object lock) {
    this.lock = lock;
  } 
  // 转账
  void transfer(Account target, int amt){
    // 此处检查所有对象共享的锁
    synchronized(lock) {
      if (this.balance > amt) {
        this.balance -= amt;
        target.balance += amt;
      }
    }
  }
}

更好的方案如下

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

作者认为,如何保护多个资源,关键是要分析多个资源之间的关系。
如果资源之间没有关系,很好处理,每个资源一把锁就可以了。
如果资源之间有关联关系,就要选择一个粒度更大的锁,这个锁应该能够覆盖所有相关的资源。
除此之外,还要梳理出有哪些访问路径,所有的访问路径都要设置合适的锁。