怎么用一把锁保护多个资源?
当我们要保护多个资源时,首先要区分这些资源是否存在关联关系。
不同的资源用不同的锁保护,各自管各自的。
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这一把锁来管理账户类里所有的资源:账户余额,用户密码。
但是用一把锁有一个问题,就是性能太差,会导致取款,查看余额,修改密码,查看密码这四个操作都是串行的。而我们用两把锁,取款和修改密码就可以并行的。
用不同的锁对受保护的资源进行精细化管理,能够提升性能。这种锁还有个名字:细粒度锁。
如果多个资源是有关联关系的,那这个问题就有点复杂了。
例如:银行业务里面的转账操作,账户A减少100元,账户B增加100元。这两个账户是有关联关系的。
这种有关联关系的操作,我们应该怎么去解决能?
很快想到用 synchronized 关键字来修饰一下:
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 这把锁可以保护自己的余额 this.balance ,却保护不了别人的余额 target.balance ,就像你不能用自家的锁来保护别人家的资产,也不能用自己的票来保护别人的座位一样。
之前我们说同一把锁来保护多个资源,也就是现实世界里的“包场”。
在编程里,只要我们的锁能覆盖所有受保护资源就可以了。
上面例子中,this是对象级别的锁,所以A对象和B对象都有自己的锁,如何让A对象和B对象共享一把锁呢?
可以让所有对象都持有一个唯一性的对象。
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.class 作为共享的锁。
Account.class 是所有 Account 对象共享的,而且这个对象是Java虚拟机在加载 Account 类的时候创建的,所以我们不用担心它的唯一性。使用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;
}
}
}
}
如果资源之间没有关系,很好处理,每个资源一把锁就可以了。
如果资源之间有关联关系,就要选择一个粒度更大的锁,这个锁应该能够覆盖所有相关的资源。
“原子性”的本质是什么?其实不是不可分割,不可分割只是外在表示,其本质是多个资源间有一致性的要求,操作的中间状态对外不可见。
例如:
所以,解决原子性问题,是要保证中间状态对外不可见。(黑箱操作)