首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >并发编程如何使用锁保护多个资源

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

作者头像
小土豆Yuki
发布2020-11-03 11:36:08
9480
发布2020-11-03 11:36:08
举报
文章被收录于专栏:洁癖是一只狗洁癖是一只狗

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

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

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

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

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


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没有并发问题


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

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


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


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作为共享锁,修改代码如下


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指令的,转账操作的原子性是面向高级语言的,不过本质是一样

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-11-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 洁癖是一只狗 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档