Java 互斥锁:如何用一把锁保护多个资源

Java 互斥锁:如何用一把锁保护多个资源

怎么用一把锁保护多个资源?

当我们要保护多个资源时,首先要区分这些资源是否存在关联关系。

保护没有关联关系的多个资源

不同的资源用不同的锁保护,各自管各自的。


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元。这两个账户是有关联关系的。

这种有关联关系的操作,我们应该怎么去解决能?

很快想到用 synchronized 关键字来修饰一下:

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

在这段代码中,临界区内有两个资源,分别是转出账户的余额 this.balance 和转入账户的余额 target.balance 并且用的是一把锁 this,符合我们之前提到的,多个资源可以用一把锁来保护,这看上去正确,但实际上有问题。

问题就出在 this 这把锁上,this 这把锁可以保护自己的余额 this.balance ,却保护不了别人的余额 target.balance ,就像你不能用自家的锁来保护别人家的资产,也不能用自己的票来保护别人的座位一样。

Java 互斥锁:如何用一把锁保护多个资源_第1张图片

Java 互斥锁:如何用一把锁保护多个资源_第2张图片

使用锁的正确姿势

之前我们说同一把锁来保护多个资源,也就是现实世界里的“包场”。

在编程里,只要我们的锁能覆盖所有受保护资源就可以了。

上面例子中,this是对象级别的锁,所以A对象和B对象都有自己的锁,如何让A对象和B对象共享一把锁呢?

可以让所有对象都持有一个唯一性的对象。

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

这个办法确实能解决问题,但是有点问题,它要求在创建Account对象的时候必须传入同一个对象。在真实的项目场景中,创建Account对象的代码很可能分散在多个工程中,传入共享的lock直的很难。

所以,上面的方案缺乏实践的可行性。我们需要更好的方案。还真有,就是用Account.class 作为共享的锁。

Account.class 是所有 Account 对象共享的,而且这个对象是Java虚拟机在加载 Account 类的时候创建的,所以我们不用担心它的唯一性。使用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 互斥锁:如何用一把锁保护多个资源_第3张图片

总结

如果资源之间没有关系,很好处理,每个资源一把锁就可以了。

如果资源之间有关联关系,就要选择一个粒度更大的锁,这个锁应该能够覆盖所有相关的资源。

“原子性”的本质是什么?其实不是不可分割,不可分割只是外在表示,其本质是多个资源间有一致性的要求,操作的中间状态对外不可见。

例如:

  1. 在32位机器上写long 型变量有中间状态(只写了64位中的32位)
  2. 在银行转账的操作中也有中间状态(账户A减少了100,账户B还没来得及发生变化)

所以,解决原子性问题,是要保证中间状态对外不可见。(黑箱操作)

你可能感兴趣的:(Java基础)