并发锁如何保护多个资源

753 阅读4分钟

并发锁保护多个资源

对于互斥锁而言受保护的资源和锁的关系一般都是多对一,那么如何用一把互斥锁去保护多个资源呢?

首先需要区分多个资源之间是否有关联。

多个资源之间不存在关联

多个资源之间不存在关联如球场的座位和电影院的座位它们之间不存在强关联关系,两个资源分别使用球赛的门票和电影院的门票管理即可。

例如银行中存在两种业务,银行账户余额取款业务和银行账户密码更改业务,两种资源之间并没有关联性,所以可以将两种资源分别赋予一把锁管理。

代码如下所示

 public class Account {
     // 余额
     private Integer balance;
 ​
     // 余额加锁对象
     public final Object balanceObjct = new Object();
 ​
     // 密码
     private String password;
 ​
     // 密码加锁对象
     public final Object passwordObject = new Object();
 ​
     // 取款
     public void withdraw(Integer amt){
         synchronized (balanceObjct){
             if (this.balance>amt){
                 this.balance-=amt;
             }
         }
     }
 ​
     // 查看余额
     public Integer getBalance(){
         synchronized (balanceObjct){
             return balance;
         }
     }
 ​
     // 修改密码
     public void updatePassword(String newPassword){
         synchronized (passwordObject){
             this.password = newPassword;
         }
     }
 ​
     // 查询密码
     public String getPassword(){
         synchronized (passwordObject){
             return this.password;
         }
     }
 }

当然方法不止一种,也可以将整个this作为一把锁,管理两个资源,这样写虽然方便,但是带来的问题就是锁的粒度太大,两个本来是并行的资源硬生生的变成了串行执行影响效率,所以我们在开发中应该用不同的锁对受保护的资源进行精细化管理,能提升性能

多个资源之间存在关联关系

场景引入

多个资源之间存在关联关系的处理相对复杂,如银行之间转账业务,账户A转账给账户B,账户A少100元账户B就要加上100元。

问题代码化如下

 public class Account {
     // 余额
     private Integer balance;
 ​
     // 转账业务,当前this对象账户向目标账户转账amt
     public void transfer(Account target, int amt){
     // public synchronized void transfer( Account target, int amt){
         if (this.balance > amt) { 
             this.balance -= amt; 
             target.balance += amt; 
         } 
     }
 }

拿到这样的业务,一般想当然的就是给transfer加上synchronized关键字解决并发问题,如第7行所示,但是真是这样吗?

当然不是,目标账户一直没有受到保护,它是作为一个参数在transfer方法中,加锁操作只能作用于当前的this对象,target的对象没有加锁操作。

image-20220212224112754

具体分析一下,假设有如下场景,A,B,C三个账户分别有200元的金额,A向B转账100元,B向C转账100元,最后的正确结果应该是A的余额为100元,B的余额是200元,C的余额是300元。

现在将上述需求放到上述代码中执行,目前存在两个线程,线程1执行A向B转账100元,线程2执行B向C转账100元,两个线程运行在不同的CPU上,那么线程1和线程2是否互斥呢?显然不是线程1和线程2可以同时执行,线程1和线程2读取账户B的余额到寄存器中的数据都是200,最后的结果就是账户B的余额不可能是200,可能是100(线程1的结果被线程2覆盖)也可能是300(线程2的结果被线程1覆盖)。

解决办法

其实多个关联资源去做并发控制,首先想到的一点就是锁需要覆盖所有的资源,即可解决问题。

持有相同的对象

 public class Account {
     private Integer balance;
 ​
     // 新增代码开始
     private Object lock;
 ​
     private Account(){
 ​
     }
 ​
     public Account(Object lock){
         this.lock = lock;
     }
     // 新增代码结束
 ​
     public void transfer( Account target, int amt){
         // 获取锁
         synchronized (lock){
             if (this.balance > amt) {
                 this.balance -= amt;
                 target.balance += amt;
             }
         }
     }
 }

这样做的缺陷就是在创建Account对象时需要传入同一个对象,如果传入不同对象那么就会出现锁保护对象的错乱,这个对程序稳定性不好缺乏可行性。

持有类模板对象

可以将锁对象改为Account.class,它是所有的Account所共享,能够确定唯一性。

修改代码如下。

 public class Account {
     private Integer balance;
     
     // 新增代码结束
 ​
     public void transfer( Account target, int amt){
         // 获取锁 使用类模板
         synchronized (Account.class){
             if (this.balance > amt) {
                 this.balance -= amt;
                 target.balance += amt;
             }
         }
     }
 }

image-20220212225833289

\