上一篇我们知道受保护资源和锁之间合理的关联关系应该是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指令的,转账操作的原子性是面向高级语言的,不过本质是一样
原子性的本质其实并不是不可分割,这只是他的表现,其本质是多个资源间有一致性的要求,操作的中间状态对外不可见。
本文分享自微信公众号 - 洁癖是一只狗(rookie-dog),作者:洁癖汪
原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。
原始发表时间:2020-11-02
本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。
我来说两句