写在前面

上一篇文章

上图的关键是「R1 的锁保护 R1」的指向关系是否正确

如果都是保护单个资源这样简单,程序猿的世界该有多美好,可惜并不是,通常我们需要保护多个资源

保护多个资源

保护多个没有关系的资源

如果多个资源没有关系,那就是保护一个资源模型的复制,同样非常简单,且看下图:

比如现实中银行取款和修改密码操作。
银行取款操作对应的资源是「余额」, 修改密码操作对应的资源是「密码」,余额和密码两个资源完全没有关系,所以各自用自家的锁保护自家的资源就好了

如果多个资源没有关系,程序猿的世界该有多美好,可惜并不是,我们保护的资源多数情况都有关联关系

保护多个关系的资源

拿经典的银行转账案例来说明,账户 A 给账户 B 转账,账户 A 余额减少 100 元,账户 B 余额增加 100 元,这个操作要是原子性的,那么资源「A 余额」和资源「B 余额」就这样"有了关系",先来看程序:

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

用 synchronized 直接保护 transfer 方法,然后操作资源「A 余额」和资源「B 余额」就可以了

⚠️: 真的是这样吗?

先停止向下看,在你的笔记本上按照文章开头的三步走来画个图看一看,是否和下图一样呢?

我们通常容易忽略锁和资源的指向关系,我们想当然的用锁 this 来保护 target 资源了,也就没有起到保护作用

假设 A,B,C 账户初始余额都是 200 原,A 向 B 转账 100,B 向 C 转账 100

我们期盼最终的结果是:
账户 A 余额: 100 元
账户 B 余额: 200 元
账户 C 余额: 300 元

假线程 1「A 向 B 转账」与线程 2「B 向 C 转账」两个操作同时执行,根据 JMM 模型可知,线程 1 和线程 2 读取线程 B 当前的余额都是 200 元:

所以线程 1 和线程 2 可以同时进入 transfer 临界区,上面你认为对的模型其实就会变成这个样子:

还记得 

总结

到这里关于锁和资源的关系你应该了解的更加透彻了,单个资源和多个无关联资源的情形都很好处理,为各自资源创建相应的锁就好,如果多个资源有关联,为了让锁起到保护作用,我们需要将锁的粒度变大,比如将 this 锁变成了 Account.class 锁。

转账业务非常常见,并发量非常大,如果我们将锁的粒度都提升到 Account.class 这个级别(分久必合),假设每次转账业务都很耗时,那么显然这个锁的性能是比较低的,所以接下来的文章,我们还会继续优化这个模型,选择合适的锁粒度,同时能保护多个有关联的资源

我们的锁粒度虽然大,但是我们保障了账户的安全,所以并发编程可以先保证事情做对,遇到瓶颈了,慢慢优化改变相应的模型就好了,当然熟练理解这个模型以后,一步到位的并发编程模型当然是极好的......

灵魂追问

  1. 还记得 happens-before 的几个原则吗?
  2. 偏向锁,轻量锁,重量锁是不是和我们这节内容有异曲同工之处呢?
  3. 提前想一下,我们如何来优化这个模型呢?

附加说明

如果你对这篇文章理解有些困难,可以按照下面的顺序回忆前序文章相关内容


  1. 欢迎持续关注公众号:「日拱一兵」

    • 前沿 Java 技术干货分享
    • 高效工具汇总 | 回复「工具」
    • 面试问题分析与解答
    • 技术资料领取 | 回复「资料」

    以读侦探小说思维轻松趣味学习 Java 技术栈相关知识,本着将复杂问题简单化,抽象问题具体化和图形化原则逐步分解技术问题,技术持续更新,请持续关注......

    https://www.cnblogs.com/FraserYu/p/11683761.html