一起养成写作习惯!这是我参与「日新计划 4 月更文挑战」的第19天,点击查看活动详情
本系列专栏Java并发编程专栏 – 元浩875的专栏 – ()
前言
上一篇文章我们说了解决原子性问题,我们使用互斥锁来保证共享变量同一时刻只有一个线程可以操作,然后说了编译器帮我实现加解锁的synchronized关键字,以及最重要的锁和其保护的资源之间的关系是1:N,那本章就重点说一下这个如何一把锁保护多个资源。
正文
其实这个问题是个很复杂的问题,我们从简单的来看。
保护多个没有关联的资源
假如多个资源没有关联关系,而对不同资源的操作是不需要互斥的,我们就可以创建多个锁来保护不同的资源,这种对受保护对象进行细化管理,能够提升性能,这种锁叫做细粒度锁。
比如现在有个账户类Account,里面有2个成员变量,分别是余额balance和密码password,取款withdraw()和查看账户余额getBalance()会访问余额balance,我们创建一个balLock锁来保护balance这个资源;而更改密码updatePassword()和查看密码getPassword()会修改账户密码password,我们创建一个pwLock锁来保护password资源。
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,这2个账户是有关联关系的,那这种如何解决呢
首先创建一个Account类,该类有一个成员变量余额:balance,还有一个用于转账的方法: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;
}
}
}
这个代码中,我们用当前对象this做为锁,但是临界区中却有2个资源,分别转出账户余额:this.balance和转入账户余额:target.balance,也就是我们用一把锁来保护多个资源,这真的可靠吗
其实不可靠,this这把锁只能保护自己的余额,却无法保护别人的余额,示意图如下:
我们来举个例子,假设有A、B、C三个账户,余额都是200,现在有2个线程分别执行转账操作:A转账给B 100,B转账给C 100,最后期望结果是A余额100,B余额200,C余额300。
假设线程1执行A给B转账,线程2执行B给C转账,这2个线程分别在2颗CPU上执行,线程1锁定的对象是A.this,线程2锁定的对象是B.this,这2个线程同时可以进入临界区,这时就有问题了,线程1和线程2都能读取到B的值是200,但是在执行完方法后,B的值可能是100,可能是300。
当线程1后于线程2写B.balance,线程2写的B.balance值就被线程1覆盖,也就是300;当线程2后于线程1写B.balance,线程1写的B.balance值被线程2覆盖,也就是100。
这里出现问题的原因就是锁所保护的资源不对。
使用锁的正确方法
解决上面问题的方法我们可以使用覆盖一个全部资源的锁,在前面的例子中,this是对象级别的锁,所以A对象和B对象都有自己的锁,如何使用一个共同的锁呢 我们可以使用Account.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;
}
}
}
}
然后这里就可以一个锁保护了多个受保护资源,示意图如下:
但是这里也有个很明显的问题,比如A给B转账,C给D转账,这本来是2个不相关的操作,但是由于使用了Accout.class作为锁,就会导致这2个操作是阻塞的,性能很差。
总结
本章说了一把锁保护多个资源的情况,当资源之间没有联系,可以细化锁;当多个资源有联系时,使用锁就需要额外注意了。上面最后的例子也存在严重的性能问题,具体如何优化,我们后面文章再讨论。