并发编程如何使用锁保护多个资源

2020-11-03 11:36:08 浏览数 (3)

上一篇我们知道受保护资源和锁之间合理的关联关系应该是N:1的关系,也就是说一个锁可以保护多个资源,并不能多把锁来保护一个资源,今天我们就说说如何实现一把锁保护多个资源.

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

在现实生活中,球场的座位和电影院的座位是没有关联的,这种场景非常容易解决,那就是球场有球场的门票,电影院有电影的门票

同样,在编程的世界里,也是同样的原理,比如,银行业务的针对账户余额的取款操作,和银行账户密码的修改,我们可以为余额和密码分别分配不同的锁,

比如下面的代码,Account有两个成员,分别是余额balance和账户密码password,取款和查看余额会操作账户余额,我们专门创建一个final对象balLock作为锁,而更高密码我们也专门创建一个pwlock作为锁,不同的资源用不通的锁,各自管理各自的资源

代码语言:javascript复制

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这一把锁来管理账户所有资源,只要给多有方法添加synchronized,就可以了

但是用一把锁保护所有资源,性能方便太差,所有的资源操作都是必须串行进行的,而我们用两把锁,取款和修改密码是可以并行的,用不同锁对受保护资源进行精细化管理,能够提升性能,这个锁的名字叫细粒度锁

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

多个资源有关联,是不容处理的,比如,三个账户A,B,C,我们在账户A里减少100元,给账户B加100元,这两个账户就是有关联的,看下面代码,一个账户对象,有一个成员变量余额,还有一个转账的方法transfer,如何保证转账transfer没有并发问题

代码语言:javascript复制

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

我们第一时间想到的方案就是添加synchronized,如下图

代码语言:javascript复制

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

这个方案看上去好像没有问题,但是我要知道使用synchronized其实使用this这把锁,问题就出现在this,this只能保护自己的月this.balance,却保护不了别人的余额target.balance,就像你用自己家的锁去保护别人家的锁,也不能用自己的票来保护别人的座位一样

举个例子,我们有三个账户A,B,C,余额都是200元,我们用两个线程分别操作:账户A给账户B转100元,账户B给账户C转100元,最后我们期待的是,账户A是100元,账户B是200元,账户C是300元

我们使用的两个线程,不同的CPU,这样是不能达到互斥的,因为线程1锁定的是账户A,而线程2锁定的是账户B,所以这两个线程同时进入临界区transfer,就有可能是300(线程1后于线程2,线程2写的余额值会被覆盖)也有可能是100(线程1先于线程2,线程1写的余额值会被覆盖),但是就是没有200.

使用锁的正确姿势

如果解决上面的问题呢,我们就可以使用同一把锁保护多个资源,也就是现实世界的包场,那么上面的例子中,this是对象级别的锁,但是账户A和账户B是不同的对象,如何可以共享一把锁呢

我们其实可以让所有对象都持有一个唯一性的对象,这个对象再创建Account时传入,如下面代码,我们把Account默认构造函数改成private,同时增加一个带object lock参数的构造函数,在创建Account对象是,传入相同的lock,这样所有的Account对象都共享一把锁lock

代码语言:javascript复制

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真的很难.上面的可行性是不行的,那么我们有没有更好的方案呢,当时是有的,就是使用Account.class,而且这个对象是java虚拟机在加载Account类创建的,可以保证他就是唯一的,使用Account,class作为共享锁,修改代码如下

代码语言:javascript复制

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

下图很直观的展示了我们是如何使用Account.class实现保护不同对象临界区

最后,我们重申一下关联关系,其实就是原子性的特征,之前我们说的原子性,主要是面向CPU指令的,转账操作的原子性是面向高级语言的,不过本质是一样

原子性的本质其实并不是不可分割,这只是他的表现,其本质是多个资源间有一致性的要求,操作的中间状态对外不可见。

0 人点赞