在上一篇文章中,我们提到受保护资源和锁之间合理的关联关系应该是 N:1 的关系,也就是说可以用一把锁来保护多个资源,但是不能用多把锁来保护一个资源,并且结合文中示例,我们也重点强调了“不能用多把锁来保护一个资源”这个问题。而至于如何保护多个资源,我们今天就来聊聊。

保护没有关联关系的多个资源

在现实世界里,球场的座位和电影院的座位就是没有关联关系的,这种场景非常容易解决,那就是球赛有球赛的门票,电影院有电影院的门票,各自管理各自的。

相关的示例代码如下,账户类 Account 有两个成员变量,分别是账户余额 balance 和账户密码 password。取款 withdraw() 和查看余额 getBalance() 操作会访问账户余额 balance,我们创建一个 final 对象 balLock 作为锁(类比球赛门票);而更改密码 updatePassword() 和查看密码 getPassword() 操作会修改账户密码 password,我们创建一个 final 对象 pwLock 作为锁(类比电影票)。不同的资源用不同的锁保护,各自管各自的,很简单

  1. class Account {
  2. // 锁:保护账户余额
  3. private final Object balLock
  4. = new Object();
  5. // 账户余额
  6. private Integer balance;
  7. // 锁:保护账户密码
  8. private final Object pwLock
  9. = new Object();
  10. // 账户密码
  11. private String password;
  12. // 取款
  13. void withdraw(Integer amt) {
  14. synchronized(balLock) {
  15. if (this.balance > amt){
  16. this.balance -= amt;
  17. }
  18. }
  19. }
  20. // 查看余额
  21. Integer getBalance() {
  22. synchronized(balLock) {
  23. return balance;
  24. }
  25. }
  26. // 更改密码
  27. void updatePassword(String pw){
  28. synchronized(pwLock) {
  29. this.password = pw;
  30. }
  31. }
  32. // 查看密码
  33. String getPassword() {
  34. synchronized(pwLock) {
  35. return password;
  36. }
  37. }
  38. }

保护有关联关系的多个资源

如果多个资源是有关联关系的,那这个问题就有点复杂了。例如银行业务里面的转账操作,账户 A 减少 100 元,账户 B 增加 100 元。这两个账户就是有关联关系的。那对于像转账这种有关联关系的操作,我们应该怎么去解决呢?先把这个问题代码化。我们声明了个账户类:Account,该类有一个成员变量余额:balance,还有一个用于转账的方法:transfer(),然后怎么保证转账操作 transfer() 没有并发问题呢?

  1. class Account {
  2. private int balance;
  3. // 转账
  4. synchronized void transfer(
  5. Account target, int amt){
  6. if (this.balance > amt) {
  7. this.balance -= amt;
  8. target.balance += amt;
  9. }
  10. }
  11. }

下面我们具体分析一下,假设有 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。

04 | 互斥锁(下):如何用一把锁保护多个资源? - 图1

在上一篇文章中,我们提到用同一把锁来保护多个资源,也就是现实世界的“包场”,那在编程领域应该怎么“包场”呢?很简单,只要我们的锁能覆盖所有受保护资源就可以了。在上面的例子中,this 是对象级别的锁,所以 A 对象和 B 对象都有自己的锁,如何让 A 对象和 B 对象共享一把锁呢?

  1. class Account {
  2. private Object lock
  3. private int balance;
  4. private Account();
  5. // 创建 Account 时传入同一个 lock 对象
  6. public Account(Object lock) {
  7. this.lock = lock;
  8. }
  9. // 转账
  10. void transfer(Account target, int amt){
  11. // 此处检查所有对象共享的锁
  12. synchronized(lock) {
  13. if (this.balance > amt) {
  14. this.balance -= amt;
  15. target.balance += amt;
  16. }
  17. }
  18. }
  19. }
  1. class Account {
  2. private int balance;
  3. // 转账
  4. void transfer(Account target, int amt){
  5. synchronized(Account.class) {
  6. if (this.balance > amt) {
  7. this.balance -= amt;
  8. target.balance += amt;
  9. }
  10. }
  11. }
  12. }