Java并发编程实战 03互斥锁 解决原子性问题
文章系列
Java并发编程实战 01并发编程的Bug源头
Java并发编程实战 02Java如何解决可见性和有序性问题
摘要
在上一篇文章02Java如何解决可见性和有序性问题当中,我们解决了可见性和有序性的问题,那么还有一个原子性问题咱们还没解决。在第一篇文章01并发编程的Bug源头当中,讲到了把一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性,那么原子性的问题该如何解决。
同一时刻只有一个线程执行这个条件非常重要,我们称为互斥,如果能保护对共享变量的修改时互斥的,那么就能保住原子性。
简易锁
我们把一段需要互斥执行的代码称为临界区,线程进入临界区之前,首先尝试获取加锁,若加锁成功则可以进入临界区执行代码,否则就等待,直到持有锁的线程执行了解锁unlock()操作。如下图:

但是有两个点要我们理解清楚:我们的锁是什么?要保护的又是什么?
改进后的锁模型
在并发编程世界中,锁和锁要保护的资源是有对应关系的。
首先我们需要把临界区要保护的资源R标记出来,然后需要创建一把该资源的锁LR,最后针对这把锁,我们需要在进出临界区时添加加锁lock(LR)操作和解锁unlock(LR)操作。如下:

Java语言提供的锁技术:synchronized
synchronized可修饰方法和代码块。加锁lock()和解锁unlock()都会在synchronized修饰的方法或代码块前后自动加上加锁lock()和解锁unlock()操作。这样做的好处就是加锁和解锁操作会成对出现,毕竟忘了执行解锁unlock()操作可是会让其他线程死等下去。
那我们怎么去锁住需要保护的资源呢?在下面的代码中,add1()非静态方法锁定的是this对象(当前实例对象),add2()静态方法锁定的是X.class(当前类的Class对象)
public class X {
public synchronized void add1() {
// 临界区
}
public synchronized static void add2() {
// 临界区
}
}
上面的代码可以理解为这样:
public class X {
public synchronized(this) void add() {
// 临界区
}
public synchronized(X.class) static void add2() {
// 临界区
}
}
使用synchronized 解决 count += 1 问题
在01 并发编程的Bug源头文章当中,我们提到过count += 1 存在的并发问题,现在我们尝试使用synchronized解决该问题。
public class Calc {
private int value = 0;
public synchronized int get() {
return value;
}
public synchronized void addOne() {
value += 1;
}
}
addOne()方法被synchronized修饰后,只有一个线程能执行,所以一定能保证原子性,那么可见性问题呢?在上一篇文章02 Java如何解决可见性和有序性问题当中,提到了管程中的锁规则,一个锁的解锁 Happens-Before 于后续对这个锁的加锁。管程,在这里就是synchronized(管程的在后续的文章中介绍)。根据这个规则,前一个线程执行了value += 1操作是对后续线程可见的。而查看get()方法也必须加上synchronized修饰,否则也没法保证其可见性。
上面这个例子如下图:

那么可以使用多个锁保护一个资源吗,修改一下上面的例子后,get()方法使用this对象锁来保护资源value,addOne()方法使用Calc.class类对象来保护资源value,代码如下:
public class Calc {
private static int value = 0;
public synchronized int get() {
return value;
}
public static synchronized void addOne() {
value += 1;
}
}
上面的例子用图来表示:

在这个例子当中,get()方法使用的是this锁,addOne()方法使用的是Calc.class锁,因此这两个临界区(方法)并没有互斥性,addOne()方法的修改对get()方法是不可见的,所以就会导致并发问题。
结论:不可使用多把锁保护一个资源,但能使用一把锁保护多个资源(这里没写例子,只写了一把锁保护一个资源)
保护没有关联关系的多个资源
在银行的业务当中,修改密码和取款是两个再经常不过的操作了,修改密码操作和取款操作是没有关联关系的,没有关联关系的资源我们可以使用不同的互斥锁来解决并发问题。代码如下:
public class Account {
// 保护密码的锁
private final Object pwLock = new Object();
// 密码
private String password;
// 保护余额的锁
private final Object moneyLock = new Object();
// 余额
private Long money;
public void updatePassword(String password) {
synchronized (pwLock) {
// 修改密码
}
}
public void withdrawals(Long money) {
synchronized (moneyLock) {
// 取款
}
}
}
分别使用pwLock和moneyLock来保护密码和余额,这样修改密码和修改余额就可以并行了。使用不同的锁对受保护的资源进行进行更细化管理,能够提升性能,这种锁叫做细粒度锁。
在这个例子当中,你可能发现我使用了final Object来当成一把锁,这里解释一下:使用锁必须是不可变对象,若把可变对象作为锁,当可变对象被修改时相当于换锁,而且使用Long或Integer作为锁时,在-128到127之间时,会使用缓存,详情可查看他们的valueOf()方法。
保护有关联关系的多个资源
在银行业务当中,除了修改密码和取款的操作比较多之外,还有一个操作比较多的功能就是转账。账户 A 转账给 账户B 100元,账户A的余额减少100元,账户B的余额增加100元,那么这两个账户就是有关联关系的。在没有理解互斥锁之前,写出的代码可能如下:
public class Account {
// 余额
private Long money;
public synchronized void transfer(Account target, Long money) {
this.money -= money;
if (this.money < 0) {
// throw exception
}
target.money += money;
}
}
在转账transfer方法当中,锁定的是this对象(用户A),那么这里的目标用户target(用户B)的能被锁定吗?当然不能。这两个对象是没有关联关系的。正确的操作应该是获取this锁和target锁才能去进行转账操作,正确的代码如下:
public class Account {
// 余额
private Long money;
public synchronized void transfer(Account target, Long money) {
synchronized(this) {
synchronized (target) {
this.money -= money;
if (this.money < 0) {
// throw exception
}
target.money += money;
}
}
}
}
在这个例子当中,我们需要清晰的明白要保护的资源是什么,只要我们的锁能覆盖所有受保护的资源就可以了。
但是你以为这个例子很完美?那就错了,这里面很有可能会发生死锁。你看出来了吗?下一篇文章我就用这个例子来聊聊死锁。
总结
使用互斥锁最最重要的是:我们的锁是什么?锁要保护的资源是什么?,要理清楚这两点就好下手了。而且锁必须为不可变对象。使用不同的锁保护不同的资源,可以细化管理,提升性能,称为细粒度锁。
参考文章:
极客时间:Java并发编程实战 03互斥锁(上)
极客时间:Java并发编程实战 04互斥锁(下)
个人博客网址: https://colablog.cn/
如果我的文章帮助到您,可以关注我的微信公众号,第一时间分享文章给您

Java并发编程实战 03互斥锁 解决原子性问题的更多相关文章
- 《Java并发编程实战》笔记-锁与原子变量性能比较
如果线程本地的计算量较少,那么在锁和原子变量上的竞争将非常激烈.如果线程本地的计算量较多,那么在锁和原子变量上的竞争会降低,因为在线程中访问锁和原子变量的频率将降低. 在高度竞争的情况下,锁的性能将超 ...
- Java并发编程实战(chapter_1)(原子性、可见性)
混混噩噩看了很多多线程的书籍,一直认为自己还不够资格去阅读这本书.有种要高登大堂的感觉,被各种网络上.朋友.同事一顿外加一顿的宣传与传颂,多多少少再自我内心中产生了一种敬畏感.2月28好开始看了之后, ...
- Java并发编程实战 04死锁了怎么办?
Java并发编程文章系列 Java并发编程实战 01并发编程的Bug源头 Java并发编程实战 02Java如何解决可见性和有序性问题 Java并发编程实战 03互斥锁 解决原子性问题 前提 在第三篇 ...
- Java并发编程实战 05等待-通知机制和活跃性问题
Java并发编程系列 Java并发编程实战 01并发编程的Bug源头 Java并发编程实战 02Java如何解决可见性和有序性问题 Java并发编程实战 03互斥锁 解决原子性问题 Java并发编程实 ...
- Java并发编程实战 02Java如何解决可见性和有序性问题
摘要 在上一篇文章当中,讲到了CPU缓存导致可见性.线程切换导致了原子性.编译优化导致了有序性问题.那么这篇文章就先解决其中的可见性和有序性问题,引出了今天的主角:Java内存模型(面试并发的时候会经 ...
- 【Java并发编程实战】----- AQS(二):获取锁、释放锁
上篇博客稍微介绍了一下AQS,下面我们来关注下AQS的所获取和锁释放. AQS锁获取 AQS包含如下几个方法: acquire(int arg):以独占模式获取对象,忽略中断. acquireInte ...
- 【java并发编程实战】-----线程基本概念
学习Java并发已经有一个多月了,感觉有些东西学习一会儿了就会忘记,做了一些笔记但是不系统,对于Java并发这么大的"系统",需要自己好好总结.整理才能征服它.希望同仁们一起来学习 ...
- java并发编程实战学习(3)--基础构建模块
转自:java并发编程实战 5.3阻塞队列和生产者-消费者模式 BlockingQueue阻塞队列提供可阻塞的put和take方法,以及支持定时的offer和poll方法.如果队列已经满了,那么put ...
- 【Java并发编程实战】-----“J.U.C”:ReentrantReadWriteLock
ReentrantLock实现了标准的互斥操作,也就是说在某一时刻只有有一个线程持有锁.ReentrantLock采用这种独占的保守锁直接,在一定程度上减低了吞吐量.在这种情况下任何的"读/ ...
随机推荐
- Python Requests-学习笔记(10)-会话对象
会话对象让你能够跨请求保持某些参数.它也会在同一个Session实例发出的所有请求之间保持cookies. 会话对象具有主要的Requests API的所有方法. 我们来跨请求保持一些cookies: ...
- Chrome浏览器架构
通用浏览器架构 它可以是一个具有许多不同线程的进程,也可以是具有几个通过IPC进行通信的多个线程的进程. 一个具有许多不同线程的进程 通过IPC进行通信的多个线程的进程 注意 这些不同的体系结构是实现 ...
- SpringMVC中利用HandlerExceptionResolver完成异常处理
在解决Controller层中的异常问题时,如果针对每个异常处理相对较为繁琐.在SpringMVC中提供了HandlerExceptionResolver用于处理捕获到的异常,从而重新定义返回给前端的 ...
- springboot项目war包部署及出现的问题Failed to bind properties under 'mybatis.configuration.mapped-statements[0].
1.修改pom文件 修改打包方式 为war: 添加tomcat使用范围,provided的意思即在发布的时候有外部提供,内置的tomcat就不会打包进去 <groupId>com.scho ...
- AJ学IOS(26)UI之iOS抽屉效果小Demo
AJ分享,必须精品 先看效果 实现过程 第一步,把三个view设置好,还有颜色 #warning 第一步 - (void)addChildView { // left UIView *leftView ...
- 通过神秘代码登录自己的QQ
通过神秘代码登录自己的QQ 如图:是怎么做成的呢? 其实很明显,这个是QQ号码的16进制 所以网上搜索进制转换打开一个网页 输入自己的QQ号码,得到16进制的QQ号码 在QQ页面账号处先添加0x然后复 ...
- Python 编程环境搭建(Windows 系统中)
由于大家普遍使用 Windows 系统,所以本文只介绍 Windows 系统中 Python 环境的安装. 在 Windows 中安装 Python 与安装普通软件没什么差别,下载所需版本的安装包后, ...
- PDF各种骚操作如何用python实现
前言 文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理. 作者: wLsq PS:如有需要Python学习资料的小伙伴可以加点击下方链 ...
- java面试题(一年工作经验)的心得
看面试题 正常人第一步肯定都会看面试题,我也不例外,在看的过程中,我发现有些文章写的不错,对我帮助不小值得推荐,如下: Java面试题全集(上) 很多基础的东西,建议先看. 各大公司Java后端开发面 ...
- 词向量表示:word2vec与词嵌入
在NLP任务中,训练数据一般是一句话(中文或英文),输入序列数据的每一步是一个字母.我们需要对数据进行的预处理是:先对这些字母使用独热编码再把它输入到RNN中,如字母a表示为(1, 0, 0, 0, ...