美文网首页
Java并发之如何用一把锁保护多个资源(互斥锁,下)

Java并发之如何用一把锁保护多个资源(互斥锁,下)

作者: WAHAHA402 | 来源:发表于2019-04-21 00:29 被阅读0次

上篇文章中提到受保护的资源和锁之间合理的关系应该是 N:1 的关系,也就是说用一把锁来保护多个资源。
分两种情况:

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

对比现实生活,球场的座位和电影院的座位没有关联关系,这种场景非常容易解决,就是球场有球场的门票,电影院有电影院的门票,各管各的。
对应到编程领域,两个没有关联关系的对象用不同的锁来解决并发问题。
下面一段代码中,账户密码和余额没有关联关系。

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修饰,锁为this。但是性能太差。用不同的锁来对资源进行精细化管理,能提升性能。这种锁,叫细粒度锁

2.保护有关联关系的多个资源

例如两个账户A和B,A转给B100元,A余额减少100,B余额增加一百。这俩账户是有关联关系的。以下为账户类代码:

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

那么如何解决并发,是下面这样吗?

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

看似没有问题,但如果你真的觉得没有问题,说明还没有理解锁和资源的对应关系。synchronized关键字在这里锁的是this对象,却无法锁住target账户对象。this这把锁能保护自己的余额balance,却保护不了别人的余额。看图:


用锁 this 保护 this.balance 和 target.balance的示意图

假设还有一个账户C,初始每个账户均有200,现在线程1执行A转账给B,线程2执行B转账给C,这俩线程分别同时在两个CPU上执行,他们是互斥的吗?我们期望是,但实际上不是。两个线程分别同时锁定A的实例和B的实例,同时进入临界区,同时读到B的余额为200,所以最终结果是B可能是为100,也可能为300,就是不可能为200。


并发转账示意图

使用锁的正确姿势

那么如何用一把锁保护多个资源呢,对比现实世界的“包场”,只要我们的锁能覆盖所有受保护的资源就可以了。这里介绍两种方式:
1.不同对象共享同一把锁,所有对象持有一个唯一的对象就可以了。

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对象时候必须传入同一个对象,如果传入的不是同一个lock,那就gg了。而且现实项目中,有时传入共享的lock真的很难,因此它缺乏的实践的可行性。
2.使用XXX.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并发之如何用一把锁保护多个资源(互斥锁,下)

      本文链接:https://www.haomeiwen.com/subject/ycgrgqtx.html