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

在上一篇文章中,我们提到受保护的资源和锁之间合理的关联关系应该是N:1 的关系,也就是说可以用一把锁来保护多个资源,但是不能用多把锁来保护一个资源,并且结合文中实例,我们也重点强调不用用多把锁来保护一个资源,如何保护多个资源呢, 就是我们接下来讨论的问题。

当我们要保护多个资源时,首先要区分这些资源是否存在关联关系。

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

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

同样对应到编程领域,也容易解决,例如:银行业务中有针对账户余额的操作,也有针对账户密码的操作,我们可以为账户余额和账户密码分配不同的锁来解决并发的问题。

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

public class Account {
    /**
     * 账户余额
     */
    private Integer      balance;
    /**
     * 账户余额锁
     */
    private final Object balLock = new Object();
    /**
     * 账户密码
     */
    private String       password;

    private final Object pwdLock = new Object();

    /**
     * 取款
     *
     * @param amount
     */
    void withdraw(Integer amount) {
        synchronized (balLock) {
            if (amount < balance) {
                this.balance -= amount;
            }
        }
    }

    /**
     * 查看余额
     *
     * @return
     */
    Integer getBalance() {
        synchronized (balLock) {
            return this.balance;
        }
    }

    /**
     * 更新密码
     *
     * @param pwd
     */
    void updatePassword(String pwd) {
        synchronized (pwdLock) {
            this.password = pwd;
        }
    }

    /**  
     *
     * @return
     */
    String getPassword() {
        synchronized (pwdLock) {
            return this.password;
        }
    }
}

当然我们也可以一把锁来保护多个资源,例如可以用this这一把锁来保护账户里的所有资源:余额和账户密码,具体实现很简单,所有方法增加synchronized关键字就好了。

但是用一把锁有一个问题就是性能太差,会导致存取款、修改密码、查看秘密、查看余额、四个操作都是串行的。而我们用两把锁,取款跟修改密码是可以并行的。用不同的锁对不同的资源进行精细化管理提升系统性能,这个锁有个名字:细粒度锁。

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

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

class Account {
  private int balance;
  // 转账
  void transfer(
      Account target, int amt){
    if (this.balance > amt) {
      this.balance -= amt;
      target.balance += amt;
    }
  } 
}

相信你的直觉会告诉你这样的解决方案:用户 synchronized 关键字修饰一下 transfer() 方法就可以了,于是你很快就完成了相关的代码,如下所示。

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,就像你不能用自家的锁来保护别人家的资产,也不能用自己的票来保护别人的座位一样。

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

下面我们具体分析一下,假设有 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|互斥锁(下):如何用一把锁保护多个资源?

使用锁的正确姿势

在上一篇文章中,我们提到用一把锁来保护多个资源,也即是现实世界中的包场,那在编程领域改怎么做呢?只要我们的锁能覆盖所有受保护的资源就可以。在上面的例子中,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;
      }
    }
  }
}

这个办法确实能解决问题,但是有点小瑕疵,它要求在创建 Account 对象的时候必须传入同一个对象,如果创建 Account 对象时,传入的 lock 不是同一个对象,那可就惨了,会出现锁自家门来保护他家资产的荒唐事。在真实的项目场景中,创建 Account 对象的代码很可能分散在多个工程中,传入共享的 lock 真的很难。

所以,上面的方案缺乏实践的可行性,我们需要更好的方案。还真有,就是用 Account.class 作为共享的锁。Account.class 是所有 Account 对象共享的,而且这个对象是 Java 虚拟机在加载 Account 类的时候创建的,所以我们不用担心它的唯一性。使用 Account.class 作为共享的锁,我们就无需在创建 Account 对象时传入了,代码更简单。

所以,上面的方案缺乏实践的可行性,我们需要更好的方案。还真有,就是用 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;
      }
    }
  } 
}

下面这幅图很直观地展示了我们是如何使用共享的锁 Account.class 来保护不同对象的临界区的。

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

总结

看完这篇,对于如何保护多个资源已经有心得了,关键要分析多个资源之间的关系,如果每个资源之间没有关系,每个资源一把锁就可以了,如果资源之间有关系,就选择一个粒度更大的锁,这个锁要覆盖所有的资源。



展开阅读全文

页面更新:2024-02-28

标签:都会   资源   临界   余额   线程   球赛   账户   电影院   门票   对象   密码   关系   操作   代码   方案   科技

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top