赞
踩
上一篇文章中提到一把锁可以保护多个资源,受保护资源和锁之间合理的关联关系应该是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这一把锁来管理账户类里所有的资源;账户余额和用户密码。具体实现很简单,示例程序中所有的方法都增加同步关键字synchronized就可以了,这里不一一展示了。。。
但是缺点也是存在的就是性能太差,像取款和修改密码是可以并行的,最好使用两把锁。。
用不同的锁对受保护资源进行精细化管理,能够提升性能。。 这样锁还叫细粒度锁。
上面说的问题是保护没有关联的多个资源,现在的问题有点复杂了那就是保护多个有关联关系的资源。。例如银行的转账操作:账户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
下面具体分析一下,假设有A、B、C三个账户,余额都是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对象时,传入的lock不是同一个对象会出现锁自家门来保护他家资产的事,真实项目场景中,创建Account对象的代码很可能分散在多个工程中,传入共享的lock真的很难。。
更好的方案是:用Account.class作为共享的锁。Account.class是所有Account对象共享的,而这个对象也是JVM在加载Account时候创建的,所以不用担心唯一性,代码更简单。
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来保护不同对象的临界区的。。
“原子性本质是什么?” 其实是不可分割,不可分割只是外在表现,其本质是多个资源键有一致性要求,操作的中间状态对外不可见。例如在32位机器上写long型变量有中间状态(只写了64位中的32位),所以解决原子性问题是要保证中间状态对外不可见。。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。