之前的文章里也说了,一把锁可以保护多个资源,所以受保护的资源和锁之间合理的关联关系应该是N:1的关系,上次我们直说了如何正确保护一个资源,但是没说如何正确保护多个资源,我们上次最后一个案例也说,两把锁保护两个资源,一个this,一个所属类,由于不互斥,所以会造成并发问题。
而产生问题,最主要的原因是,this对象和所属类存在必然关联的关系。
这也是今天要说的问题了,当我们要保护多个资源的时候,必然要分清楚,保护的资源们是否有关联。
保护没有关联的多个资源
比如说,球赛的门票和电影院的座位是没关系的,球赛的门票只能关联球赛的座位而不是电影院的座位。
然后同样应用到编程领域来说,例如从银行取款就会扣钱,更改账户密码就会改变,这就是两个没关系的资源。
代码如下,账户类Account有两个成员变量,分别是账户余额balance和账户密码password。取 款withdraw()和查看余额getBalance()操作会访问账户余额balance,我们创建一个final对象balLock作为锁 (类比球赛门票);而更改密码updatePassword()和查看密码getPassword()操作会修改账户密码 password,我们创建一个final对象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这一把锁,把账户类的资源(账户余额和用户密码)锁住,实现很简单,就是每个方法加一个sync就可以了。
但是有个缺点就是,性能差,而且,取款和改密码之类的应该是并行的,而不是串行,所以最好使用两把锁。
用不同的锁对受保护资源进行精细化管理,能够提升性能。这样的锁,叫做细粒度锁。
下面再来介绍一种复杂情况,保护有关联的资源
保护有关联的多个资源
之前说的问题是保护没有关联的多个资源,现在要说一种很复杂的情况,就是保护关联的资源。假如支付宝打款,A扣款100,B收款100.那么就是有关联关系的了。我们假设一个账户类:Account,该类有一个成员变量余额:balance,还有一个用于转账的方法:transfer(),然后怎么保证转账操作transfer()没有并发问题呢?
class Account {
private int balance;
// 转账
void transfer(Account target, int amt){
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
相信你的直觉会告诉你这样的解决方案:用户synchronized关键字修饰一下transfer()方法就可以了,于是 你很快就完成了相关的代码,如下所示。
class Account {
private int balance;
// 转账
synchronized void transfer(Account target, int amt){
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
但是这段代码中,临界区内有两个资源,分别是转出账户的余额this.balance和转入账户的余额target.balance,并且用的都是一把this锁,而问题也就出在这里。
this可以保护this.balance是没问题的,但是无法保护target.balance:
然后我们来具体分析一下,如果有ABC三个账户,余额都是200元,我们用两个线程分别执行两个转账操作:账户A转给账户B 100 账户B转给账户C 100,最后我们期望的结果是账户A的余额是100,账户B的余额200,账户C的余额是300.
我们假设一个线程1执行账户A转账户B的操作,一个线程2执行账户B转给账户C的操作。这两个线程在两个CPU上同时执行,所以不是互斥的。因为线程1锁定的是账户A的实例,也就是A.this,而线程2锁定的是账户B的实例,也就是B.this,所以这两个线程可以同时进入临界区transfer()。同时进入临界区的结果是什么呢?线程1和线程2都会读到账户B的余额是200,导致会出错误。
有可能线程1后于线程2写B.balance,线程2写的B.balance被线程1覆盖,也有可能是100,那就是相反过来执行,反正不可能是200.
使用锁的正确姿势
在上一篇文章中,我们用同一把锁保护多个资源,而实际上,只是我们的锁能够把资源覆盖起来,也就能保护了。在上面的例子中,this是对象级别的锁,所以A对象和B对象都有自己的锁,如何让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的实例的时候,传入的不是同一个对象,就会出现一定高度问题,而在真正的项目中,又很少会传入同一个对象进去,所以还有其他的方案。
那就是用Account.class作为共享锁 ,因为Account.class是所有Account对象共享的,而这个对象也是jvm在加载Account的时候创建的,所以我们不用担心它的2唯一性。且代码更简单。
class Account {
private int balance;
// 转账
void transfer(Account target, int amt){
synchronized(Account.class) {
if (this.balance > amt){
this.balance -= amt;
target.balance += amt;
}
}
}
}