美文网首页程序员
并发编程-用一把锁保护多个资源

并发编程-用一把锁保护多个资源

作者: 我可能是个假开发 | 来源:发表于2023-02-10 11:06 被阅读0次

一、定义

受保护资源和锁之间合理的关联关系应该是 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 元
并发转账.png

假设线程 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

相关文章

网友评论

    本文标题:并发编程-用一把锁保护多个资源

    本文链接:https://www.haomeiwen.com/subject/jvnqkdtx.html