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

    一把锁可以保护多个资源,但是不能用多把锁来保护一个资源。

那如何保护多个资源?

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

  如下代码

 1 class Account {
2 // 锁:保护账户余额
3 private final Object balLock = new Object();
4 // 账户余额
5 private Integer balance;
6 // 锁:保护账户密码
7 private final Object pwLock = new Object();
8 // 账户密码
9 private String password;
10
11 // 取款
12 void withdraw(Integer amt) {
13 synchronized(balLock) {
14 if (this.balance > amt){
15 this.balance -= amt;
16 }
17 }
18 }
19 // 查看余额
20 Integer getBalance() {
21 synchronized(balLock) {
22 return balance;
23 }
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 }

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

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

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

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

  在王老师写到的例子中有这样一个案例分析:

 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。

  我把这个图重新画了一遍,应该能更贴合老师的意思:

   注意红框处,代表线程执行结束,对应的也就是案例中加粗的位置。

  为什么结果会是这个样子?

  因为balance属于成员变量,被线程共享(线程在执行各自的方法时,对含有成员变量操作的方法会将成员变量拷贝到自己的工作内存<栈>进行操作),所以各自线程都只会操作各自的balance,而线程2的执行结果虽然写回到了主存(hp原则,解锁操作的结果对后续加锁操作可见),但是由于线程1执行完了后也会写回主存,所以导致线程2的balance被线程1的balance覆盖。

  

使用锁的正确姿势

  所以该如何解决上述问题呢?

  很简单,只要我们的锁能覆盖所有受保护资源就可以了。在上面的例子中,this 是对象级别的锁,所以 A 对象和 B 对象都有自己的锁,如何让 A 对象和 B 对象共享一把锁呢?

  比如可以让所有对象都持有一个唯一性的对象,这个对象在创建 Account 时传入。

  示例代码如下,我们把 Account 默认构造函数变为 private,同时增加一个带 Object lock 参数的构造函数,创建 Account 对象时,传入相同的 lock,这样所有的 Account 对象都会共享这个 lock 了。(怎么保证传入的这个lock是同一个lock?)

  在真实的项目场景中,创建 Account 对象的代码很可能分散在多个工程中,传入共享的 lock 真的很难。

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

  使用 Account.class 作为共享的锁,我们就无需在创建 Account 对象时传入了,代码更简单。

 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 }

  但是,使用Account.class获得锁,那所有转账操作都成串行了,这里实践中不可行,下一篇笔记讲优化。

总结

  如果资源之间没有关系,很好处理,每个资源一把锁就可以了。如果资源之间有关联关系,就要选择一个粒度更大的锁,这个锁应该能够覆盖所有相关的资源。除此之外,还要梳理出有哪些访问路径,所有的访问路径都要设置合适的锁。

  “原子性”的本质是什么?其实不是不可分割,不可分割只是外在表现,其本质是多个资源间有一致性的要求,操作的中间状态对外不可见

  解决原子性问题,是要保证中间状态对外不可见。

课后思考

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

  不行。this.balance 和this.password都属于可变对象,均不能作为锁。

引自极客时间用户

  可以在Account中添加一个静态object,通过锁这个object来实现一个锁保护多个资源,如下:

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

  老师回复:这种方式比锁class更安全(???why),因为这个锁是私有的。有些最佳实践要求必须这样做。

摘自极客时间王宝令老师的课程

java并发编程实战《四》互斥锁(下)的更多相关文章

  1. Java并发编程实战 03互斥锁 解决原子性问题

    文章系列 Java并发编程实战 01并发编程的Bug源头 Java并发编程实战 02Java如何解决可见性和有序性问题 摘要 在上一篇文章02Java如何解决可见性和有序性问题当中,我们解决了可见性和 ...

  2. Java并发编程(四)锁的使用(上)

    锁的作用 锁是一种线程同步机制,用于实现互斥,当线程占用一个对象锁的时候,其它线程如果也想使用这个对象锁就需要排队.如果不使用对象锁,不同的线程同时操作一个变量的时候,有可能导致错误.让我们做一个测试 ...

  3. 《Java并发编程实战》笔记-锁与原子变量性能比较

    如果线程本地的计算量较少,那么在锁和原子变量上的竞争将非常激烈.如果线程本地的计算量较多,那么在锁和原子变量上的竞争会降低,因为在线程中访问锁和原子变量的频率将降低. 在高度竞争的情况下,锁的性能将超 ...

  4. Java并发编程实战 04死锁了怎么办?

    Java并发编程文章系列 Java并发编程实战 01并发编程的Bug源头 Java并发编程实战 02Java如何解决可见性和有序性问题 Java并发编程实战 03互斥锁 解决原子性问题 前提 在第三篇 ...

  5. Java并发编程实战 05等待-通知机制和活跃性问题

    Java并发编程系列 Java并发编程实战 01并发编程的Bug源头 Java并发编程实战 02Java如何解决可见性和有序性问题 Java并发编程实战 03互斥锁 解决原子性问题 Java并发编程实 ...

  6. 【Java并发编程实战】----- AQS(二):获取锁、释放锁

    上篇博客稍微介绍了一下AQS,下面我们来关注下AQS的所获取和锁释放. AQS锁获取 AQS包含如下几个方法: acquire(int arg):以独占模式获取对象,忽略中断. acquireInte ...

  7. 【Java并发编程实战】----- AQS(四):CLH同步队列

    在[Java并发编程实战]-–"J.U.C":CLH队列锁提过,AQS里面的CLH队列是CLH同步锁的一种变形.其主要从两方面进行了改造:节点的结构与节点等待机制.在结构上引入了头 ...

  8. 【Java并发编程实战】—– AQS(四):CLH同步队列

    在[Java并发编程实战]-–"J.U.C":CLH队列锁提过,AQS里面的CLH队列是CLH同步锁的一种变形. 其主要从双方面进行了改造:节点的结构与节点等待机制.在结构上引入了 ...

  9. 【Java并发编程实战】-----“J.U.C”:ReentrantReadWriteLock

    ReentrantLock实现了标准的互斥操作,也就是说在某一时刻只有有一个线程持有锁.ReentrantLock采用这种独占的保守锁直接,在一定程度上减低了吞吐量.在这种情况下任何的"读/ ...

  10. 【java并发编程实战】-----线程基本概念

    学习Java并发已经有一个多月了,感觉有些东西学习一会儿了就会忘记,做了一些笔记但是不系统,对于Java并发这么大的"系统",需要自己好好总结.整理才能征服它.希望同仁们一起来学习 ...

随机推荐

  1. Codeforces 1404 D. Game of Pairs

    Codeforces 1404 D.Game of Pairs 给定\(2n\)个数\(1,2,...,2n\),A 和 B 将进行交互,规则如下: A 需要将元素分成 n 组 \(\mathbf{p ...

  2. XML转换成TXT行数据的Java程序

    ZKe ------------------- XML数据的一个块内的所有属性,转换成TXT文件的一行.众所周知XML文件是通过类似HTML的标签进行数据的定义如图所示 属性由id, article, ...

  3. Activit的心路历程:获取当前节点的下一节点【可能存在多个】的nodeId

    上一任务节点 在我的开发任务中,突然给我提出了一个待办任务需要获取当前任务节点下一任务节点的表单信息,刚开始搞得我有点措手不及,后来仔细是靠后,灵感一下,直接操作流程的bpmn信息就可以获取到节点信息 ...

  4. 编程,向内存0:200~0:23F依次传送数据0~63(3FH),程序中只能使用9条指令,9条指令包括 mov ax,4c00h 和 int 21h

    assume cs:code code segment mov bx,020H mov ds,bx mov bx,0 mov cx,63 s:mov [bx],bx inc bx loop s mov ...

  5. tp3.2自动验证

    <?php namespace Home\Model; use Think\Model; class UserModel extends Model{ protected $patchValid ...

  6. 性能工具-mem

    1.目前valgrind . memleak .free .top  .ps 中vsz Rss . buddy. slab 这些用的比较多,一般用于处理内存紧张问题

  7. 广度优先遍历&深度优先遍历

    一.广度优先算法BFS(Breadth First Search) 基本实现思想 (1)顶点v入队列. (2)当队列非空时则继续执行,否则算法结束. (3)出队列取得队头顶点v: (4)查找顶点v的所 ...

  8. binary hacks读数笔记(堆、栈 VMA的分布)

    一.首先看一个简单的程序: #include<stdlib.h> int main() { while(1) { sleep(1000); } return 0; } gcc -stati ...

  9. CentOS GRUB损坏修复方法

    前言 博客很久没有更新了,一个原因就是原来存放部署博客的环境坏了,硬盘使用的是SSD,只要读取到某个文件,整个磁盘就直接识别不到了,还好博客环境之前有做备份,最近一直没有把部署环境做下恢复,今天抽空把 ...

  10. Python_异常处理、调试

    1.try except 机制 # 错误处理 # 一般程序都要用到错误捕获,当没有加且有错误的时候Python解释器会执行错误捕获,且是一层层向上捕获[所以问题点会在最下面] try: print(' ...