java并发编程实战《四》互斥锁(下)
互斥锁(下):如何用一把锁保护多个资源?
一把锁可以保护多个资源,但是不能用多把锁来保护一个资源。
那如何保护多个资源?
当我们要保护多个资源时,首先要区分这些资源是否存在关联关系。
如下代码
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并发编程实战《四》互斥锁(下)的更多相关文章
- Java并发编程实战 03互斥锁 解决原子性问题
文章系列 Java并发编程实战 01并发编程的Bug源头 Java并发编程实战 02Java如何解决可见性和有序性问题 摘要 在上一篇文章02Java如何解决可见性和有序性问题当中,我们解决了可见性和 ...
- Java并发编程(四)锁的使用(上)
锁的作用 锁是一种线程同步机制,用于实现互斥,当线程占用一个对象锁的时候,其它线程如果也想使用这个对象锁就需要排队.如果不使用对象锁,不同的线程同时操作一个变量的时候,有可能导致错误.让我们做一个测试 ...
- 《Java并发编程实战》笔记-锁与原子变量性能比较
如果线程本地的计算量较少,那么在锁和原子变量上的竞争将非常激烈.如果线程本地的计算量较多,那么在锁和原子变量上的竞争会降低,因为在线程中访问锁和原子变量的频率将降低. 在高度竞争的情况下,锁的性能将超 ...
- Java并发编程实战 04死锁了怎么办?
Java并发编程文章系列 Java并发编程实战 01并发编程的Bug源头 Java并发编程实战 02Java如何解决可见性和有序性问题 Java并发编程实战 03互斥锁 解决原子性问题 前提 在第三篇 ...
- Java并发编程实战 05等待-通知机制和活跃性问题
Java并发编程系列 Java并发编程实战 01并发编程的Bug源头 Java并发编程实战 02Java如何解决可见性和有序性问题 Java并发编程实战 03互斥锁 解决原子性问题 Java并发编程实 ...
- 【Java并发编程实战】----- AQS(二):获取锁、释放锁
上篇博客稍微介绍了一下AQS,下面我们来关注下AQS的所获取和锁释放. AQS锁获取 AQS包含如下几个方法: acquire(int arg):以独占模式获取对象,忽略中断. acquireInte ...
- 【Java并发编程实战】----- AQS(四):CLH同步队列
在[Java并发编程实战]-–"J.U.C":CLH队列锁提过,AQS里面的CLH队列是CLH同步锁的一种变形.其主要从两方面进行了改造:节点的结构与节点等待机制.在结构上引入了头 ...
- 【Java并发编程实战】—– AQS(四):CLH同步队列
在[Java并发编程实战]-–"J.U.C":CLH队列锁提过,AQS里面的CLH队列是CLH同步锁的一种变形. 其主要从双方面进行了改造:节点的结构与节点等待机制.在结构上引入了 ...
- 【Java并发编程实战】-----“J.U.C”:ReentrantReadWriteLock
ReentrantLock实现了标准的互斥操作,也就是说在某一时刻只有有一个线程持有锁.ReentrantLock采用这种独占的保守锁直接,在一定程度上减低了吞吐量.在这种情况下任何的"读/ ...
- 【java并发编程实战】-----线程基本概念
学习Java并发已经有一个多月了,感觉有些东西学习一会儿了就会忘记,做了一些笔记但是不系统,对于Java并发这么大的"系统",需要自己好好总结.整理才能征服它.希望同仁们一起来学习 ...
随机推荐
- css3 渐变 兼容
.gradient{ background: #000000; background: -moz-linear-gradient(top, #000000 0%, #ffffff 100%); ba ...
- linux下的终端利器----tmux
转:tmux 是指通过一个终端登录远程主机并运行后,在其中可以开启多个控制台的终端复用软件.类似GNU Screen,但来自于OpenBSD,采用BSD授权.使用它最直观的好处就是,通过一个终端登录远 ...
- python详细图像仿射变换讲解
仿射变换简介 什么是放射变换 图像上的仿射变换, 其实就是图片中的一个像素点,通过某种变换,移动到另外一个地方. 从数学上来讲, 就是一个向量空间进行一次线形变换并加上平移向量, 从而变换到另外一个向 ...
- wpf 全局异常捕捉+错误日志记录+自动创建桌面图标
/// /// 创建桌面图标 /// public static void CreateShortcutOnDesktop(string LnkName) { String shortcutPath ...
- nginx&http 第三章 ngx 1-http ngx_http_wait_request_handler
对于活跃的 HTTP 连接,在执行连接建立回调函数 ngx_http_init_connection 的过程中会执行 ngx_http_wait_request_handler 回调函数, 负责 HT ...
- malloc/free与new/delete的区别(转)
相同点:都可用于申请动态内存和释放内存 不同点:(1)操作对象有所不同.malloc与free是C++/C 语言的标准库函数,new/delete 是C++的运算符.对于非内部数据类的对象而言,光用m ...
- jm8.6编解码器概述
自己在学习h264的路上,欢迎讨论交流. 前段时间研究JM出品的h264编码器,代码实在看不下去,因此换了个角度来研究诸多算法--逆向方式(解码),本系列文章记录一些遇到的东西和思考. 1. JM介绍 ...
- 常见mysql后台线程
1.IO THREAD MySQL有很多后台线程 其中包括了负责IO的相关线程IO THREAD 1. 参数innodb_write_io_threads 写线程 默认四个,负责数据块的写入 2 ...
- 内核补丁热更新ceph内核模块
前言 内核模块的更新一般需要卸载模块再加载,但是很多时候使用场景决定了无法做卸载的操作,而linux支持了热更新内核模块的功能,这个已经支持了有一段时间了,一直没有拿ceph的相关模块进行验证 准备工 ...
- Git-stash(暂存)
修改某文件后,不想commit,使用stash保存在本地的某分支内 # 暂存 git stash ## 可暂存新增文件 git stash -u ## 为此次暂存添加标识 git stash save ...
