Java并发编程实战(3)- 互斥锁
我们在这篇文章中主要讨论如何使用互斥锁来解决并发编程中的原子性问题。
概述
并发编程中的原子性问题的源头是线程切换,那么禁止线程切换可以解决原子性问题吗?
这需要分情况讨论,在单核CPU的情况下,同一时刻只有一个线程执行,禁止CPU中断,就意味着操作系统不会重新调度线程,也就禁止了线程切换,这样获取CPU使用权的线程就可以不间断的执行。
在多核CPU的情况下,同一时刻,有可能有两个线程同时执行,一个线程执行在CPU-1上,另外一个线程执行在CPU-2上,这时禁止CPU中断,只能保证某一个CPU上的线程连续执行,但并不能保证只有一个线程在运行。
同一时刻只有一个线程执行,我们称之为互斥,如果我们能够保证对共享变量的修改是互斥的,那么无论是单核CPU还是多核CPU,就都能保证原子性了。
如何能做到呢?答案就是互斥锁。
互斥锁模型
互斥锁简易模型
当我们谈论互斥锁时,我们一般会把一段需要互斥执行的代码称为临界区,下面是一个简单的示意图。
当线程进入临界区之前,首先尝试加锁,如果成功,可以进去临界区,如果失败,需要等待。当临界区的代码被执行完毕或者发生异常时,线程释放锁。
互斥锁改进模型
上面的模型虽然直观,但是过于简单,我们需要考虑2个问题:
- 我们锁的是什么?
- 我们保护的又是什么?
在现实世界中,锁和锁要保护的资源是有对应关系的,通俗的讲,你用你家的锁保护你家的东西,我用我家的锁保护我家的东西。
在并发编程的世界中,锁和资源也应该有类似的对应关系。
下面是改进后的锁模型。
首先,我们要把临界区中要保护的资源R标注出来,然后,我们为资源R创建一个锁LR,最后,在我们进入和离开临界区时,需要对锁LR进行加锁和解锁操作。
通过这样的处理,我们就在锁和资源之间建立了关联关系,不会出现类似于“用我家的锁去保护你家的资源”的问题。
Java世界中的互斥锁
在Java语言中,我们通过synchronized关键字来实现互斥锁。
synchronized关键字可以应用在方法上,也可以直接应用在代码块中。
我们来看下面的示例代码。
public class SynchronizedDemo {
// 修饰实例方法
synchronized void updateData() {
// 业务代码
}
// 修饰静态方法
synchronized static void retrieveData() {
// 业务代码
}
// 修饰代码块
Object obj = new Object();
void createData() {
synchronized(obj) {
// 业务代码
}
}
}
和我们描述的互斥锁模型相比,我们并没有在上述代码中看到加锁和解锁相关的代码,这是因为Java编译器已经自动为我们在synchronized关键字修改的方法或者代码块前后添加了加锁和解锁逻辑。这样做的好处是我们不用担心执行加锁操作后,忘了解锁操作。
synchronized中的锁和锁对象
我们在使用synchronized关键字时,它锁定的对象是什么呢?如果没有显式指定锁对象,Java有如下默认规则
- 当修饰静态方法时,锁定的是当前类的Class对象。
- 当修饰非静态方法时,锁定的是当前实例对象this。
根据上述规则,下面的代码是等价的。
// 修饰实例方法
synchronized void updateData() {
// 业务代码
}
// 修饰实例方法
synchronized(this) void updateData2() {
// 业务代码
}
// 修饰静态方法
synchronized static void retrieveData() {
// 业务代码
}
// 修饰静态方法
synchronized(SynchronizedDemo.class) static void retrieveData2() {
// 业务代码
}
synchronized示例
我们在之前的文章中描述过count=count+1的例子,当时没有做并发控制,结果引发了原子性问题,我们现在看一下,如何使用synchronized关键字来解决并发问题。
首先我们来复习一下Happens-Before规则,synchronized修饰的临界区是互斥的,也就是说同一时刻只有一个线程执行临界区的代码,而Happens-Before中的“对一个锁解锁Happens-Before后续对这个锁的加锁”,指的是前一个线程解锁操作对后一个线程的加锁操作是可见的,然后结合Happens-Before传递性原则,我们可以得出前一个线程在临界区修改的共享变量,对于后续完成加锁进入临界区的线程是可见的。
下面是修改后的代码:
public class ConcurrencySafeAddDemo {
private long count = 0;
private synchronized void safeAdd() {
int index = 0;
while (index < 10000) {
count = count + 1;
index++;
}
}
private void reset() {
this.count = 0;
}
private void addTest() throws InterruptedException {
List<Thread> threads = new ArrayList<Thread>();
for (int i = 0; i < 6; i++) {
threads.add(new Thread(() -> {
this.safeAdd();
}));
}
for (Thread thread : threads) {
thread.start();
}
for (Thread thread : threads) {
thread.join();
}
threads.clear();
System.out.println(String.format("Count is %s", count));
}
public static void main(String[] args) throws InterruptedException {
ConcurrencySafeAddDemo demoObj = new ConcurrencySafeAddDemo();
for (int i = 0; i < 10; i++) {
demoObj.addTest();
demoObj.reset();
}
}
}
执行结果如下。
Count is 60000
Count is 60000
Count is 60000
Count is 60000
Count is 60000
Count is 60000
Count is 60000
Count is 60000
Count is 60000
Count is 60000
这里和我们的预期是一致的。
和第一版的代码相比,我们只是用synchronized关键字修饰了safeAdd()方法。
锁与受保护的资源的关系
对于互斥锁来说,锁与受保护的资源之间的关联关系非常重要,那么这两者之间到底是什么关系呢?一个合理的解释是:锁与受保护的资源之间是N:1的关系,也就是说:
- 一个锁可以应用到多个受保护资源
- 一个受保护资源上只能有一个锁
我们可以用球赛门票来做类比,其中座位是资源,门票是锁。一个座位只能用一张门票来保护,如果是“包场”的情况,一张包场门票就可以对应多个座位。不会出现一个座位有多张门票的情况。
同理,在互斥锁的场景下,如果两个锁使用了不同的锁对象,那么这两个所对应的临界区不是互斥的。 这一点很重要,忽视它的话,很容易引发莫名其妙的并发问题。
例如,我们把上面示例代码中的safeAdd()方法改成下面的样子,它还能正常工作吗?
private void safeAdd() {
int index = 0;
synchronized(new Object()) {
while (index < 10000) {
count = count + 1;
index++;
}
}
}
这里,我们在为synchronized关键字设置锁对象时,每次都新建一个Object对象,那么每个线程在运行到这里时,都是使用不同的锁对象,那么临界区中的代码就不是互斥的,最后得出的结果也不会是我们期望的。
Count is 17355
Count is 18215
Count is 19244
Count is 20863
Count is 60000
Count is 60000
Count is 60000
Count is 20430
Count is 60000
Count is 60000
一个锁保护多个资源
上面我们谈到一个互斥锁可以保护多个资源,但是一个资源不可以被多个互斥锁保护。
那么,我们如何用一个锁来保护多个资源呢?
一个锁保护多个没有关联关系的资源
对于多个没有关联关系的资源,我们很容易用一个锁去保护。
以银行账户为例,银行账户可以有取款操作,也有修改密码操作,那么账户余额和账户密码就是两个没有关联关系的资源。
我们来看下面的示例代码。
public class BankAccountLockDemo {
private double balance;
private String password;
private Object commonLockObj = new Object();
// 取钱
private void withdrawMoney(double amount) {
synchronized(commonLockObj) {
// 业务代码
balance = balance - amount;
}
}
// 修改密码
private void changePassword(String newPassword) {
synchronized(commonLockObj) {
// 业务代码
password = newPassword;
}
}
}
我们可以看到,上述代码使用了共享锁commonLockObj
来保护balance和password,是可以正常工作的。
但是这样做存在的问题是取款和修改密码操作不能同时进行,从业务角度看,这两块业务是没有关联的, 应该是可以并行的。
解决办法是每个业务使用各自的互斥锁对相关资源进行保护。上述代码中可以创建两个锁对象:balanceLockObj
和passwordLockObj
,这样两个业务操作就不会互相影响了,这样的锁也被称为细粒度锁。
一个锁保护多个有关联关系的资源
对于有关联关系的资源,情况会复杂一些。
我们以转账操作为例进行说明,转账的过程会涉及两个账户的余额,这两个余额就是两个有关联关系的资源。
我们来看下面的示例代码。
public class BankAccountTransferLockDemo {
private double balance;
private Object lockObj = new Object();
private void transfer(BankAccountTransferLockDemo sourceAccount, BankAccountTransferLockDemo targetAccount, double amount) {
synchronized(lockObj) {
sourceAccount.balance = sourceAccount.balance - amount;
targetAccount.balance = targetAccount.balance + amount;
}
}
}
上述代码有问题吗? 答案是有问题。
看上去我们在操作balance的时候,使用了加锁处理,但是需要注意这里的锁对象是lockObj
,是一个Object对象,如果此时有其他业务也需要操作相同账户的balance,例如存取款操作,其他业务是没有办法使用lockObj
来创建锁的,从而造成多个业务同时操作balance,引发并发问题。
问题的解决办法是我们创建的锁需要能够覆盖受保护资源的所有场景。
回到我们上面的示例,如果使用Object对象作为锁对象不能覆盖所有相关业务,那么我们需要升级锁对象,将其由Object对象变为Class对象,代码如下:
private void transfer(BankAccountTransferLockDemo sourceAccount, BankAccountTransferLockDemo targetAccount, double amount) {
synchronized(BankAccountTransferLockDemo.class) {
sourceAccount.balance = sourceAccount.balance - amount;
targetAccount.balance = targetAccount.balance + amount;
}
}
上述资源之间的关联关系,如果用更具体、更专业的语言来描述,其实是一种“原子性”的特征,原子性有两层含义:1) CPU指令级别的原子性,2)业务含义上的原子性。
“原子性”的本质什么?
原子性的表象是不可分割,其本质是多个资源间有一致性的要求,操作的中间状态对外不可见。
解决原子性问题,就是要保证中间状态对外不可见,这也是互斥锁要解决的问题。
Java并发编程实战(3)- 互斥锁的更多相关文章
- Java并发编程实战 03互斥锁 解决原子性问题
文章系列 Java并发编程实战 01并发编程的Bug源头 Java并发编程实战 02Java如何解决可见性和有序性问题 摘要 在上一篇文章02Java如何解决可见性和有序性问题当中,我们解决了可见性和 ...
- 《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并发编程实战】-----“J.U.C”:ReentrantReadWriteLock
ReentrantLock实现了标准的互斥操作,也就是说在某一时刻只有有一个线程持有锁.ReentrantLock采用这种独占的保守锁直接,在一定程度上减低了吞吐量.在这种情况下任何的"读/ ...
- 【Java并发编程实战】-----“J.U.C”:Semaphore
信号量Semaphore是一个控制访问多个共享资源的计数器,它本质上是一个"共享锁". Java并发提供了两种加锁模式:共享锁和独占锁.前面LZ介绍的ReentrantLock就是 ...
- 【Java并发编程实战】-----“J.U.C”:ReentrantLock之一简介
注:由于要介绍ReentrantLock的东西太多了,免得各位客官看累,所以分三篇博客来阐述.本篇博客介绍ReentrantLock基本内容,后两篇博客从源码级别分别阐述ReentrantLock的l ...
- 【java并发编程实战】-----线程基本概念
学习Java并发已经有一个多月了,感觉有些东西学习一会儿了就会忘记,做了一些笔记但是不系统,对于Java并发这么大的"系统",需要自己好好总结.整理才能征服它.希望同仁们一起来学习 ...
- java并发编程实战学习(3)--基础构建模块
转自:java并发编程实战 5.3阻塞队列和生产者-消费者模式 BlockingQueue阻塞队列提供可阻塞的put和take方法,以及支持定时的offer和poll方法.如果队列已经满了,那么put ...
随机推荐
- PyQt(Python+Qt)学习随笔:Qt Designer中QAbstractButton派生按钮部件autoRepeat、autoRepeatDelay、autoRepeatInterval属性
autoRepeat.autoRepeatDelay.autoRepeatInterval这三个属性为一组属性,用于控制按钮的按下事件是否重复.重复的频次等. autoRepeat 如果启用了auto ...
- 前端性能测试(H5性能测试)
前端性能调优方法同样适用于H5. 1.H5前端性能知识点 学习前端性能,必须对HTTP协议有所了解. 1.1 浏览器渲染引擎 浏览器是Html解析和页面最终展示的工具. 浏览器的主要功能:将用户选择的 ...
- 痞子衡嵌入式:了解i.MXRT1060系列ROM中串行NOR Flash启动初始化流程优化点
大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家分享的是i.MXRT1060系列ROM中串行NOR Flash启动初始化流程优化点. 前段时间痞子衡写了一篇 <深入i.MXRT1050系 ...
- Scrum 冲刺第二天
一.每日站立式会议 1.会议内容 1)进行每日工作汇报 张博愉: 昨天已完成的工作:制定测试计划.博客编写 今日工作计划:测试mappe里的接口 工作中遇到的困难:对测试接触得较少 张润柏: 昨天已完 ...
- 【Codeforces 1083C】Max Mex(线段树 & LCA)
Description 给定一颗 \(n\) 个顶点的树,顶点 \(i\) 有点权 \(p_i\).其中 \(p_1,p_2,\cdots, p_n\) 为一个 \(0\sim (n-1)\) 的一个 ...
- VirtualBox5.2.2 安装 CentOS 7
转自百度经验:https://jingyan.baidu.com/article/4dc4084868a1e4c8d946f133.html?tdsourcetag=s_pctim_aiomsg&am ...
- tengine下载和安装
tengine简介: Tengine所基于开发的Nginx的意思是Engine-X,Tengine在淘宝开发,所以我们把Engine-X中的X替换成Taobao.Tengine即Taobao-Engi ...
- Windows版 charles安装证书抓包网页HTTPS
1.在Charles官网https://www.charlesproxy.com/download/下载,我这边下载的是免费体验版的. 2.安装好以后打开,配置Charles证书:选择 help--S ...
- MySQL全备及备份文件删除脚本
1.数据库全备 #!/bin/shv_user="root"v_password="mysql"backup_date=`date +%Y%m%d%H%M` M ...
- gnuplot添加直线和箭头
http://blog.csdn.net/bill_chuang/article/details/18215051 6.在图中添加直线和箭头 gnuplot> set arrow from 0. ...