一、定义
受保护资源和锁之间合理的关联关系应该是 N: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;
}
}
}
账户类 Account 有两个成员变量:
- 账户余额 balance
- 账户密码 password
取款 withdraw() 和查看余额 getBalance() 操作会访问账户余额 balance,创建一个 final 对象 balLock 作为锁;
更改密码 updatePassword() 和查看密码 getPassword() 操作会修改账户密码 password,创建一个 final 对象 pwLock 作为锁。
不同的资源用不同的锁保护,各自管各自的。
可以用一把互斥锁来保护多个资源,例如可以用 this 这一把锁来管理账户类里所有的资源:账户余额和用户密码。具体实现很简单,示例程序中所有的方法都增加同步关键字 synchronized 就可以了。
细粒度锁
用一把锁有个问题,就是性能太差,会导致取款、查看余额、修改密码、查看密码这四个操作都是串行的。而用两把锁,取款和修改密码是可以并行的。用不同的锁对受保护资源进行精细化管理,能够提升性能。这种锁叫细粒度锁。
三、保护有关联关系的多个资源
1.案例一
如果多个资源是有关联关系的,例如银行业务里面的转账操作,账户 A 减少 100 元,账户 B 增加 100 元。这两个账户就是有关联关系的。
声明一个账户类:Account,该类有一个成员变量余额:balance,还有一个用于转账的方法:
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,就像你不能用自家的锁来保护别人家的资产,也不能用自己的票来保护别人的座位一样。
用锁 this 保护 this.balance 和 target.balance.png
2.案例二
假设有 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,导致最终账户 B 的余额可能是 300(线程 1 后于线程 2 写 B.balance,线程 2 写的 B.balance 值被线程 1 覆盖),可能是 100(线程 1 先于线程 2 写 B.balance,线程 1 写的 B.balance 值被线程 2 覆盖),就是不可能是 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;
}
}
}
}
image.png
极客时间《Java并发编程实战》学习笔记Day06 - http://gk.link/a/11W9i
网友评论