上篇文章中提到受保护的资源和锁之间合理的关系应该是 N:1 的关系,也就是说用一把锁来保护多个资源。
分两种情况:
1.保护没有关联关系的多个资源
对比现实生活,球场的座位和电影院的座位没有关联关系,这种场景非常容易解决,就是球场有球场的门票,电影院有电影院的门票,各管各的。
对应到编程领域,两个没有关联关系的对象用不同的锁来解决并发问题。
下面一段代码中,账户密码和余额没有关联关系。
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;
}
}
}
当然我们也可以用一把锁来保护,实现方法是所有的方法用synchronized修饰,锁为this。但是性能太差。用不同的锁来对资源进行精细化管理,能提升性能。这种锁,叫细粒度锁。
2.保护有关联关系的多个资源
例如两个账户A和B,A转给B100元,A余额减少100,B余额增加一百。这俩账户是有关联关系的。以下为账户类代码:
class Account {
private int balance;
// 转账
void transfer(
Account target, int amt){
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
那么如何解决并发,是下面这样吗?
class Account {
private int balance;
// 转账
synchronized void transfer(
Account target, int amt){
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
看似没有问题,但如果你真的觉得没有问题,说明还没有理解锁和资源的对应关系。synchronized关键字在这里锁的是this对象,却无法锁住target账户对象。this这把锁能保护自己的余额balance,却保护不了别人的余额。看图:
用锁 this 保护 this.balance 和 target.balance的示意图
假设还有一个账户C,初始每个账户均有200,现在线程1执行A转账给B,线程2执行B转账给C,这俩线程分别同时在两个CPU上执行,他们是互斥的吗?我们期望是,但实际上不是。两个线程分别同时锁定A的实例和B的实例,同时进入临界区,同时读到B的余额为200,所以最终结果是B可能是为100,也可能为300,就是不可能为200。
并发转账示意图
使用锁的正确姿势
那么如何用一把锁保护多个资源呢,对比现实世界的“包场”,只要我们的锁能覆盖所有受保护的资源就可以了。这里介绍两种方式:
1.不同对象共享同一把锁,所有对象持有一个唯一的对象就可以了。
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,那就gg了。而且现实项目中,有时传入共享的lock真的很难,因此它缺乏的实践的可行性。
2.使用XXX.class
这里Account.class是所有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;
}
}
}
}
这个算是更好的方法,但是有个现实的问题你有注意到吗?
所有的转账操作都变成串行的了。还能同时转账吗?
网友评论