点击上方蓝字关注我们

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

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

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

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

同样这对应到编程领域,也很容易解决。例如,银行业务中有针对账户余额(余额是一种资源)的取款操作,也有针对账户密码(密码也是一种资源)的更改操作,我们可以为账户余额和账户密码分配不同的锁来解决并发问题,这个还是很简单的。

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

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; } } }

当然,我们也可以用一把互斥锁来保护多个资源,例如我们可以用 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,就像你不能用自家的锁来保护别人家的资产,也不能用自己的票来保护别人的座位一样。

用锁 this 保护 this.balance 和 target.balance 的示意图

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

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

所以,上面的方案缺乏实践的可行性,我们需要更好的方案。还真有,就是用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 来保护不同对象的临界区的。

总结

相信你看完这篇文章后,对如何保护多个资源已经很有心得了,关键是要分析多个资源之间的关系。如果资源之间没有关系,很好处理,每个资源一把锁就可以了。如果资源之间有关联关系,就要选择一个粒度更大的锁,这个锁应该能够覆盖所有相关的资源。除此之外,还要梳理出有哪些访问路径,所有的访问路径都要设置合适的锁,这个过程可以类比一下门票管理。

我们再引申一下上面提到的关联关系,关联关系如果用更具体、更专业的语言来描述的话,其实是一种“原子性”特征,在前面的文章中,我们提到的原子性,主要是面向 CPU 指令的,转账操作的原子性则是属于是面向高级语言的,不过它们本质上是一样的。

“原子性”的本质是什么?其实不是不可分割,不可分割只是外在表现,其本质是多个资源间有一致性的要求,操作的中间状态对外不可见。例如,在 32 位的机器上写 long 型变量有中间状态(只写了 64 位中的 32 位),在银行转账的操作中也有中间状态(账户 A 减少了 100,账户 B 还没来得及发生变化)。所以解决原子性问题,是要保证中间状态对外不可见。

课后思考

在第一个示例程序里,我们用了两把不同的锁来分别保护账户余额、账户密码,创建锁的时候,我们用的是:private final Object xxxLock = new Object();,如果账户余额用 this.balance 作为互斥锁,账户密码用 this.password 作为互斥锁,你觉得是否可以呢?

欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。

(如有侵权,请联系小编删除谢谢。)

留言区

互斥锁必须用同一个吗_04 | 互斥锁(下):如何用一把锁保护多个资源?相关推荐

  1. java 无锁缓存_如何在高并发环境下设计出无锁的数据库操作(Java版本)

    一个在线2k的游戏,每秒钟并发都吓死人.传统的hibernate直接插库基本上是不可行的.我就一步步推导出一个无锁的数据库操作. 1. 并发中如何无锁. 一个很简单的思路,把并发转化成为单线程.Jav ...

  2. Redis:分布式锁setnx(只 实现了 互斥性和容错性)

    Redis:分布式锁setnx(只 实现了 互斥性和容错性) 关键词 同时在redis上创建同一个key,只有一个能成功,即获得锁 获取锁:原子性操作 set方法(推荐),谁set成功谁获取到锁(过期 ...

  3. Linux内核中锁机制之完成量、互斥量

    在上一篇博文中笔者分析了关于信号量.读写信号量的使用及源码实现,接下来本篇博文将讨论有关完成量和互斥量的使用和一些经典问题. 八.完成量 下面讨论完成量的内容,首先需明确完成量表示为一个执行单元需要等 ...

  4. C# 乐观锁、悲观锁、共享锁、排它锁、互斥锁

    悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁.传统的关系型数据 ...

  5. python 线程锁_python多线程编程(3): 使用互斥锁同步线程

    问题的提出 上一节的例子中,每个线程互相独立,相互之间没有任何关系.现在假设这样一个例子:有一个全局的计数num,每个线程获取这个全局的计数,根据num进行一些处理,然后将num加1.很容易写出这样的 ...

  6. 自旋锁与互斥锁的区别,信号量和互斥锁的区别

    文章目录 自旋锁与互斥锁的区别 信号量和互斥锁的区别 什么是信号量 什么是互斥锁 主要区别 生产者-消费者问题 使用mutex 使用信号量 信号量的优点 mutex的优点 信号量的缺点 mutex的缺 ...

  7. 磁盘一把锁一个感叹号_TBase中的一些锁

    1.前言 锁在生活中随处可见,电子锁.挂锁.门锁等等.在数据库中,同样也存在着各式各样的锁,表级锁.行级锁.页锁等等,数据库的并发能力除了和它的并发控制机制有关, 还和数据库的锁粒度控制息息相关,粒度 ...

  8. 并发系列三:证明分代年龄、无锁、偏向锁、轻量锁、重(chong)偏向、重(chong)轻量、重量锁

    前言 上篇文章咱们了解了synchronized关键字的常见用法.对象头以及证明了一个对象在无锁状态下的对象头markwork部分的前56位存储的是hashcode.接下来,咱们继续来根据对象头分别证 ...

  9. 学习笔记:MySQL高阶知识体系(下)——索引、锁、日志、隔离级别与MVCC

    转载自https://www.ydlclass.com/doc21xnv/database/mysqladvance/mysqlAdvance2.html MySQL高阶知识体系(下) 6. 索引 6 ...

最新文章

  1. BYOD革命挑战企业IT安全
  2. “别人家的小孩”是如何用一行代码手撕面试题的?
  3. WPF自定义控件(1)——仪表盘设计[1]
  4. MATLAB与图像处理(二):批处理读取多张图片,cell
  5. 一段话系列-领域模型是什么?
  6. java vector_Java Vector elements()方法与示例
  7. java二维数组遍历排序,实现二维数组的按次序排序!!!
  8. 电脑底部任务栏点不动_虫洞 跨平台的电脑控制手机软件
  9. DOM节点的属性及文本操作
  10. TCP协议与UDP协议的区别
  11. 认证服务器的搭建_这个认证正式发布!
  12. idea 背景色修改_IDEA使用调优配置
  13. 设计素材网址集合(比较全)
  14. 计算机系统领域顶级会议--OSDI,SOSP
  15. Verilog时钟n分频
  16. 云计算发展现状及未来趋势
  17. 怎么查ip地址 多种方法教学
  18. 第一(关于list及dict)
  19. html 导航栏设计,在网页中设计导航菜单的三个原则(附案例)
  20. Android手机听筒和扬声器切换

热门文章

  1. 埃斯顿工业机器人控制柜_埃斯顿“王牌”——焊接机器人“隐形冠军”CLOOS出征...
  2. linux ubutu书籍,Ubuntu Linux入门到精通[图书]
  3. java nat 端口转发_NAT网络地址转换——静态NAT,端口映射(实操!!)
  4. html表格接收json数据,Bootstrap-table如何显示后台传过来的JSON数据?
  5. c语言射击类小游戏任务书,(c语言课程设计报告小游戏“石头剪子布”.doc
  6. Linux 内核参数及Oracle相关参数调整
  7. mysql数据还原时报错_还原mysql数据库时报错
  8. 分类数据显示功能_缓存优化
  9. PGSQL-通过SQL语句来计算两个日期相差的天数
  10. android源码出现的@字符代表什么意思