深入理解Java并发框架AQS系列(四):共享锁(Shared Lock)
深入理解Java并发框架AQS系列(一):线程
深入理解Java并发框架AQS系列(二):AQS框架简介及锁概念
深入理解Java并发框架AQS系列(三):独占锁(Exclusive Lock)
深入理解Java并发框架AQS系列(四):共享锁(Shared Lock)
一、前言
那些“简单的”并发代码背后,隐藏着大量信息。。。
独占锁虽说在j.u.c
中有现成的实现,但在JAVA的语言层面也同样提供了支持(synchronized
);但共享锁却是只存在于AQS中,而它在实际生产中的使用频次丝毫不亚于独占锁,在整个AQS体系中占有举重若轻的地位。而在某种意义上,因为可能同时存在多个线程的并发,它的复杂度要高于独占锁。本章除了介绍共享锁数据结构等,还会重点对焦并发处理,看 doug lea 在并发部分是否有遗漏
j.u.c
下支持的并发锁有Semaphore
、CountDownLatch
等,本章我们采用经典并发类Semaphore
来阐述
二、简介
共享锁其实是相对独占锁而言的,涉及到共享锁就要聊到并发度,即同一时刻最多允许同时执行线程的数量。上图所述的并发度为3,即在同一时刻,最多可有3个人在同时过河。
但共享锁的并发度也可以设置为1,此时它可以看作是一个特殊的独占锁
2.1、waitStatus
在独占锁章节中,我们介绍到了关键的状态标记字段waitStatus
,它在独占锁的取值有
0
SIGNAL (-1)
CANCELLED (1)
而这些取值在共享锁中也都存在,含义也保持一致,而除了上述这3个取值外,共享锁还额外引入了新的取值:
PROPAGATE (-3)
且-3
这个取值在整个AQS体系中,只存在于共享锁中,它的存在是为了更好的解决并发问题,我们将在后文中详细介绍
2.2、使用场景
本人参加的某性能挑战赛中,有这样一个场景:数据产生于CPU,且有12个线程在不断的制造数据,而这些数据需要持久化到磁盘中,由于数据产生的非常快,此时的瓶颈卡在IO上;磁盘的性能经过基准测试,发现每次写入8K数据,且开4个线程写入时,能将IO打满;但如何控制在同一时刻,最多有4个线程进行IO写入呢?
其实这是一个典型的使用共享锁的场景,我们用三四行代码即可解决
// 设置共享锁的并发度为4
Semaphore semaphore = new Semaphore(4);
// 加锁
semaphore.acquire();
// 执行数据存储
storeIO();
// 释放锁
semaphore.release();
三、并发
3.1、独占锁 vs 共享锁
共享锁的整体流程与独占锁相似,都是首先尝试去获取资源(子类逻辑,一般是CAS操作)
- 如果能拿到资源,那么进入同步块执行业务代码;当同步块执行完毕后,唤醒阻塞队列的头结点
- 如果资源已空,那么进入阻塞队列并挂起,等待被其他线程唤醒
两者的不同点在什么地方呢?就在于“唤醒阻塞队列的头结点”的操作。在独占锁时,唤醒头结点的操作,只会有一个线程(加锁成功的线程调用release()
)去触发;而在共享锁时,可能会有多个线程同时去调用释放
直观感觉这样设计不太合理:如果多个线程同时去唤醒头结点,而头结点只能被唤醒一次,假定阻塞队列中有20个节点,那这些节点只能等待上一个节点执行完毕后才会被唤醒,无形中共享锁的并发度变成了1。要解决这个疑问,我们先来看共享锁的释放逻辑
3.2、锁释放
先来思考一下锁释放需要做的事儿
- 1、阻塞队列的第一个节点一定要被激活;这个问题看似不值一提,却相当重要,区别于独占锁,共享锁的锁释放是存在并发的,在高并发的流量下,一定要保证阻塞队列的第一个有效节点被激活,否则会导致阻塞队列永久性的挂死
- 2、保证激活阻塞队列时的并发度;这个问题同样也是独占锁不存在的,也就是我们在3.1提出的问题;假定这样一种场景:“共享锁的并发度为10,阻塞队列中有100个待处理的节点,而此时又没有新的加锁请求,如何保证在激活阻塞队列时,保持10的并发度?”
共享锁如何解决这两个问题呢?我们接下来逐一阐述
3.2.1、调用点
与独占锁不同,共享锁调用“锁释放”有2个地方(注:AQS的一个阻塞队列是可以同时添加独占节点、共享节点的,为了简化模型,我们这里暂不讨论这种混合模型)
- a、某线程同步块执行完毕,正常调用解锁逻辑;此点与独占锁一致
- b、在每次更换头结点时,如果满足以下任一条件,同样会调用“锁释放”;更换头结点的操作,其实此时已经意味着当前线程已经加锁成功
- b.1、有额外的资源可用;拿信号量举例,当发现信号量数量>0时,表示有额外资源可用
- b.2、旧的头结点或当前头结点的
ws < 0
那这两个点调用的时候,是否存在并发呢?有同学会说“a存在并发,b是串行的”;其实此处b也是存在并发的,例如线程1更换了head节点后,准备执行“锁释放”逻辑,正在此时,线程2正常锁释放后,唤醒了新的head节点(线程3),线程3又会执行更换head节点,并准备执行“锁释放”逻辑;此时线程1跟线程3都准备执行“锁释放”逻辑
既然“锁释放”存在这么多并发,那就一定要保证“锁释放”逻辑是幂等的,那它又是如何做到呢?
3.2.1、锁释放
直接贴一下它的源码吧,释放锁的代码寥寥几笔,却很难说它简单
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
对应的流程图如下:
我们简单描述一下锁释放做的事儿
- 1、首选获取头结点的快照,并将其赋予变量
h
,同时获取h.waitStatus
,并标记位ws
- 2、判断
ws
的状态ws == -1
表示下一个节点已经挂起,或即将挂起。如果只要发现是-1状态,就进行线程唤起的话,因为存在并发,可能导致目标线程被唤起多次,故此处需要通过CAS进行抢锁,保证只有一个线程去唤起ws == 0
如果发现节点ws
为0,此处会存在两种情况(情况1:节点刚新建完毕,还未进入阻塞队列;情况2:节点由-1修改为了0),不管哪种情况,都强制将其由-1改为-3,标记位强制传播,此处是否存在漏洞?ws == -3
表示当前节点已经被标识为强制传播了,直接结束
- 3、如果此时
h == head
,说明在上述逻辑发生时,头结点没有发生变化,那么结束当前操作,否则重复上述步骤。注:AQS中所有节点只有一次当头结点的机会,也就是某个节点当过一次头结点后,便会被抛弃,再无可能第二次成为头结点,这点至关重要
根据以上分析,我们发现,节点的状态流转是通过ws
来控制的,即0、-1、-3,乍看上去,貌似不太严谨,那我们来做具体分析
3.2.2、ws
状态流转
仅有2个功能点会对ws
进行修改,一是将节点加入阻塞队列时,二就是3.2.1中描述的调用锁释放逻辑时;
我们将加入阻塞队列时ws
的状态流转再回忆下:
- 状态为0(初始状态),加入阻塞队列前,需要将前节点修改为-1,然后进入线程挂起
- 状态为-3(强制传播状态,被解锁线程标记),加入阻塞队列前,同样需要将前节点修改为-1,然后进入线程挂起
综述,我们出一张ws
的整体状态流转图
由上图可得知,只要解锁逻辑成功通过CAS将head节点由-1
修改为0
的话,那么就要负责唤醒阻塞队列中的第一个节点了
整个流转过程有bug吗?我们设想如下场景:共享锁的并发度设置为1,A、B两个线程同时进入加锁逻辑,B线程成功抢到锁,并开始进入同步块,A线程抢锁失败,准备挂到阻塞队列,正常流程是A线程将ws
由0修改为-1后,进入挂起状态,但B线程执行较快,已经优先A线程并开始执行解锁逻辑,将ws
由0修改为了-3,然后B线程正常结束;A线程发现ws
为-3后,将其修改为-1,然后进入挂起。 如果这个场景真实发生的话,A线程将永久处于挂起状态,那岂不是存在漏洞?
然而事实并非如此,因为只要A线程将ws
修改为-1后,都要再尝试进行一次获取锁的操作,正是这个操作避免了上述情况的发生,可见aqs是很严谨的
3.3、保证并发度
阻塞队列中节点的激活顺序是什么样呢?其实激活顺序3.2章节已经描述的较为清楚,解锁的逻辑只负责激活头节点,那如何保证共享锁的并发度?
我们还是假定这样一个场景:共享锁的并发度为5,阻塞队列中有20个节点,只有head节点已被唤醒,且没有新的请求进入,我们希望在同一时刻,同时有5个节点处于激活状态。针对上述场景,aqs如何做到呢?
其实head节点被激活时,在第一时间会通知后续节点,并将其唤醒,然后才会执行同步块逻辑,保证了等待中的节点快速激活
深入理解Java并发框架AQS系列(四):共享锁(Shared Lock)的更多相关文章
- 深入理解Java并发框架AQS系列(一):线程
深入理解Java并发框架AQS系列(一):线程 深入理解Java并发框架AQS系列(二):AQS框架简介及锁概念 一.概述 1.1.前言 重剑无锋,大巧不工 读j.u.c包下的源码,永远无法绕开的经典 ...
- 深入理解Java并发框架AQS系列(二):AQS框架简介及锁概念
深入理解Java并发框架AQS系列(一):线程 深入理解Java并发框架AQS系列(二):AQS框架简介及锁概念 一.AQS框架简介 AQS诞生于Jdk1.5,在当时低效且功能单一的synchroni ...
- 深入理解Java并发框架AQS系列(三):独占锁(Exclusive Lock)
一.前言 优秀的源码就在那里 经过了前面两章的铺垫,终于要切入正题了,本章也是整个AQS的核心之一 从本章开始,我们要精读AQS源码,在欣赏它的同时也要学会质疑它.当然本文不会带着大家逐行过源码(会有 ...
- 《深入理解Java集合框架》系列文章
Introduction 关于C++标准模板库(Standard Template Library, STL)的书籍和资料有很多,关于Java集合框架(Java Collections Framewo ...
- Java并发框架——AQS超时机制
AQS框架提供的另外一个优秀机制是锁获取超时的支持,当大量线程对某一锁竞争时可能导致某些线程在很长一段时间都获取不了锁,在某些场景下可能希望如果线程在一段时间内不能成功获取锁就取消对该锁的等待以提高性 ...
- Java并发框架——AQS阻塞队列管理(三)——CLH锁改造
在CLH锁核心思想的影响下,Java并发包的基础框架AQS以CLH锁作为基础而设计,其中主要是考虑到CLH锁更容易实现取消与超时功能.比起原来的CLH锁已经做了很大的改造,主要从两方面进行了改造:节点 ...
- 深入理解Java并发类——AQS
目录 什么是AQS 为什么需要AQS AQS的核心思想 AQS的内部数据和方法 如何利用AQS实现同步结构 ReentrantLock对AQS的利用 尝试获取锁 获取锁失败,排队竞争 参考 什么是AQ ...
- Java并发框架——AQS中断的支持
线程的定义给我们提供了并发执行多个任务的方式,大多数情况下我们会让每个任务都自行执行结束,这样能保证事务的一致性,但是有时我们希望在任务执行中取消任务,使线程停止.在java中要让线程安全.快速.可靠 ...
- Java并发框架——AQS之如何使用AQS构建同步器
AQS的设计思想是通过继承的方式提供一个模板让大家可以很容易根据不同场景实现一个富有个性化的同步器.同步器的核心是要管理一个共享状态,通过对状态的控制即可以实现不同的锁机制.AQS的设计必须考虑把复杂 ...
随机推荐
- Proxifier
Proxifier 使用教程 https://www.proxifier.com/ Proxifier允许不支持通过代理服务器工作的网络应用程序通过SOCKS或HTTPS代理和链进行操作. confi ...
- select notes mark
select notes mark mark-line https://time.geekbang.org/column/article/224545
- LoveWord
个人喜欢的句子汇总! 我告诉你我喜欢你,并不是一定要和你在一起,只是希望今后的你,在遭遇人生低谷的时候,不要灰心,至少曾经有人被你的魅力所吸引,曾经是,以后也会是----村上村树
- HTML页面顶部出现空白部分(#65279字符?)解决办法
1.在火狐下面用Firebug,选择body,点编辑html的时候,看到是多出了一个这个代表的意思,还真不知道,搜索后了解到是一种中文的编码规则, UTF-8不需要BOM来表明字节顺序. 制作 ...
- 人脸检测数据源制作与基于caffe构架的ALEXNET神经网络训练
本篇文章主要记录的是人脸检测数据源制作与ALEXNET网络训练实现检测到人脸(基于caffe). 1.数据获取 数据获取: ① benchmark是一个行业的基准(数据库.论文.源码.结果),例如WI ...
- 使用windbg定位内存问题【入门级】
1. 背景 在开发过程中,我们可能遇到应用程序线程占用过大的问题,可以通过windbg命令去定位哪些类型,哪些内存一直占用堆资源,从而查出问题,解决问题. 2. 准备工作 工具: 抓取DUMP文件的工 ...
- git仓库创建及基本使用
创建git用户 useradd git passwd git 创建目录 mkdir /home/git/repos/app.git/ -p 初始化目录 cd /home/git/repos/app.g ...
- HDOJ-6621(线段树+二分法)
K-th Closest Distance HDOJ-6621 本题可以使用线段树解决,结点存本结点对应的所有元素,并按照从小打到排序 最后使用二分法求解答案.因为题目中有绝对值,所以需要使用两次查找 ...
- SHELL编程概念&变量剖析
一.shell软件概念和应用场景 1) 学习Linux技术,不是为了学习系统安装.命令操作.用户权限.配置IP.网络管理,学习Linux技术重点:基于Linux系统部署和维护各种应用软件.程序(Apa ...
- 解决新版谷歌浏览器在http请求下无法开启麦克风问题
1.在浏览器地址栏中输入"chrome://flags/#unsafely-treat-insecure-origin-as-secure", 2.将该选项置为Enabled, 3 ...