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

前言

在上篇中提到,受保护资源和锁之间合理的关联关系应该是N: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关键字

但是一把锁的问题是性能太差,会导致转账、修改密码是串行的;如果用多把锁分别管理不同的操作,用不同的锁对受保护的资源进行精细化管理,能够提升性能。这个锁就叫细粒度锁

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

例如:转账操作,A用户向B用户转帐100元,A减少100,B增加100元。这两个账户就是有关联的。

示例代码:



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

方法1: 用synchronized关键字修饰一下

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

分析:
1.synchronized修饰普通方法,锁对象是this
2.在临界区有两个对象this.balance和target.balance,多个锁可以用一把锁this来保护
3.this这把锁无法保护target.balance,就像自家的锁保护不了别人家的门一样

互斥锁:如何用一把锁保护多个资源?_第1张图片
例如: A、B、C三个账户,原始资金都是200元,两个线程执行转账操作,A——>B 100元,B——>C 100元,
期望:A有100元,B有200元,C有300元。

假设有两个线程1和2执行转账操作,1执行A到B,2执行B到C,由于线程1和2是在两个CPU上执行,其实是不互斥的,线程1的锁对象是账户A的实例A.this,线程2的锁对象是B的实例B.this,所以这两个线程同时进入到临界区,transfer()。都读到B的账户余额为200,那么B的余额要么是300元(线程1后于线程2,所以target.balance=200+100),要么是100元(线程1先于线程2,target.balance=200-100),就是不会出现200元。

使用锁的正确姿势

方案一:构造方法传入同一锁

如何解决上面的问题呢?只要锁能覆盖所有受保护资源就可以了,在上面的例子中,this是对象级别的锁,所以A和B都有自己的锁A.this和B.this,如何让A和B共享一把锁就是解决问题的关键,完整代码如下:

class Account {
  private int balance;
  private final Object lock;
  private Account();
  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对象共享的,而且是在Java虚拟机加载Account类的时候创建的,所以Account.class能保证唯一性。

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

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

总结

所有的上面操作,关联关系其实是一种“原子性”特征。以前提到原子性,主要是面向CPU指令级别的,转账操作的原子性则是面向高级语言的,不过本质上是一致的(为了在线程之间切换时出现原子性问题)。

原子性的本质是:多个资源间有一致性的要求,操作的中间状态对外不可见。例如,在 32 位的机器上写 long 型变量有中间状态(只写了 64 位中的 32 位),在银行转账的操作中也有中间状态(账户 A 减少了 100,账户 B 还没来得及发生变化)。所以解决原子性问题,是要保证中间状态对外不可见。

你可能感兴趣的:(并发编程,Java并发编程实战)