被问到可重入锁条件队列,看这一篇就够了!|原创

news/2024/5/9 6:47:29/文章来源:https://blog.csdn.net/sinat_32873711/article/details/128059728

本文深入解读了高频面试点——ReentrantLock的条件队列使用方法及其原理。源码有详细注释,建议收藏阅读。

点击上方“后端开发技术”,选择“设为星标” ,优质资源及时送达

Jdk中独占锁的实现除了使用关键字synchronized外,还可以使用ReentrantLock。虽然在性能上两者没有什么区别,但ReentrantLock相比synchronized而言功能更加丰富,使用起来更为灵活,也更适合复杂的并发场景,其原理之前已经介绍过,请自行阅读。

0910213ec0fc3a10f360657aa47fcab3.jpeg

重点,一文掌握ReentrantLock加解锁原理!|原创


使用synchronized结合Object上的waitnotify方法可以实现线程间的等待通知机制。ReentrantLockCondition同样可以实现这个功能,而且相比前者使用起来更清晰也更简单。前者是java底层级别的,后者是语言级别的,后者可控制性和扩展性更好。

Condition与Object的wait/notify区别

  1. Condition能够支持不响应中断,而通过使用 Object 方式不支持

  2. Condition能够支持多个等待队列(new 多个Condition对象),而 Object 方式只能支持一个

  3. Condition能够支持超时时间的设置,而 Object 不支持

使用示例

为了方便理解源码,我们先用一个Demo展示一下ReentrantLock的线程停止和通知是如何使用的。这里使用的是一个生产者和消费者的模型,一个线程负责加,另一个线程负责减。

static volatile int i = 0;
static final ReentrantLock LOCK = new ReentrantLock();
static final Condition condition = LOCK.newCondition();public static void add() throws InterruptedException {LOCK.lock();try {while (i == 0) {Thread.sleep(1000);System.out.print("add\t");System.out.println(++i);condition.signal();condition.await();}} finally {LOCK.unlock();}
}public static void sub() throws InterruptedException {LOCK.lock();try {while (i == 1) {Thread.sleep(1000);System.out.print("sub\t");System.out.println(--i);condition.signal();condition.await();}} finally {LOCK.unlock();}
}public static void main(String[] args) throws InterruptedException {new Thread(() -> {while (true) {try {add();} catch (InterruptedException e) {e.printStackTrace();}}}).start();new Thread(() -> {while (true) {try {sub();} catch (InterruptedException e) {e.printStackTrace();}}}).start();
}

可以看到,想要获得一个Condition对象,需要首先通过一个ReentrantLock锁来创建,而最终调用其实为AQS中的内部类ConditionObject。

condition是要和lock配合使用的,而lock的实现原理又依赖于AQS,所以AQS内部实现了ConditionObject。我们知道在锁机制的实现上,AQS内部维护了一个双向的同步队列,如果是独占式锁的话,所有获取锁失败的线程的尾插入到同步队列。condition内部也是使用相似的方式,内部维护了一个单向的等待队列,所有调用condition.await方法的线程会加入到等待队列中,并且线程状态转换为等待状态。

4f5cd36022ac9d50e8352838347e0e36.png

ConditionObject中有两个成员变量:头节点firstWaiter 和 尾节点lastWaiter ,同步队列的成员Node 复用了实现同步队列的内部类Node。用nextWaiter保存了下一个等待节点,源码如下。

Condition condition = LOCK.newCondition();
//ReentrantLock内部类Sync
abstract static class Sync extends AbstractQueuedSynchronizer {final ConditionObject newCondition() {return new ConditionObject();}
}
// AQS内部类 ConditionObject
public class ConditionObject implements Condition, java.io.Serializable {/** First node of condition queue. */private transient Node firstWaiter;/** Last node of condition queue. */private transient Node lastWaiter;//真正的创建Condition对象public ConditionObject() { }
}
static final class Node {Node nextWaiter;
}

用Object的方式Object对象监视器上只能拥有一个同步队列和一个等待队列,而使用Lock可以有有一个同步队列和多个等待队列。可以多次调用lock.newCondition()创建多个Condition,所以一个Lock可以持有多个等待队列。

22418f15572bff59f199aeb81505856d.png

下面开始解读await()signal()方法。

Await方法原理

阻塞前:

1.在条件队列尾部添加新节点(状态CONDITION=-2),如果头节点为空则把当前节点设为头节点。

2.获取当前线程占有的state,无论state是几,都清空为0,代表完全释放锁。并且在释放当前线程所占用的锁之后,会唤醒同步队列中的下一个节点。

3.进入自旋判断逻辑:如果当前节点状态是 CONDITION(-2)或者 prev 节点(表示在同步队列中有前驱节点)为空,返回false,进入while逻辑,阻塞当前线程;如果有继承者,表示肯定在同步队列中,直接跳出循环;如果从同步队列队尾开始寻找,找到当前节点,同样表示在队列中,跳出循环。

bbc301f1fd2ef3345c6be2497a51f18f.png

注意!! 是先添加到条件队列,再释放锁。所以有可能出现以下的情况,A插入条件队列调用await唤醒B,但是在A唤醒后准备park时,B已经执行完需要的逻辑,并且再次Park。此时的A线程可能已经状态不再是CONDITION,说明已经进入同步队列,那就可以跳过Park再次直接争夺锁,所以这里需要自旋锁去不断尝试判断。

public final void await() throws InterruptedException {if (Thread.interrupted())throw new InterruptedException();// 1. 添加新节点,将当前线程保存其中,并且添加到等待队列队尾Node node = addConditionWaiter();// 2. 释放当前线程所占用的lock,并且唤醒同步队列中的下一个节点int savedState = fullyRelease(node);int interruptMode = 0;// 当不在同步队列中(处于condition状态或者前一个节点为null)while (!isOnSyncQueue(node)) {// 3. 当前线程进入到等待状态LockSupport.park(this);if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)break;}// 4. 自旋等待获取到同步状态(即获取到lock)if (acquireQueued(node, savedState) && interruptMode != THROW_IE)interruptMode = REINTERRUPT;if (node.nextWaiter != null) // clean up if cancelled//删除无效的等待节点unlinkCancelledWaiters();// 5. 处理被中断的情况if (interruptMode != 0)reportInterruptAfterWait(interruptMode);
}

阻塞后:

  1. 恢复执行后,检查是否中断。然后自旋再次判断是否已经进入同步队列,返回true,跳出循环继续执行。

  2. 调用acquireQueued,尝试去争夺锁,这里逻辑和lock一样,已经是同步队列去竞争锁的逻辑。并且会将之前清空的state值按照原来的大小设置。

  3. 最后都是一些中断标记的处理,主流程已经结束。

注意:退出await方法一定表明当前线程已经获得了与condition关联的锁资源。

c3373e755fa4106bdae7529903b9b55e.png

具体请看代码:

// AQS
public final void await() throws InterruptedException {if (Thread.interrupted())throw new InterruptedException();// 1. 添加新节点,将当前线程保存其中,并且添加到等待队列队尾Node node = addConditionWaiter();// 2. 释放当前线程所占用的lock,并且唤醒同步队列中的下一个节点int savedState = fullyRelease(node);int interruptMode = 0;//是先添加到等待队列,再释放锁。所以有可能出现以下的情况,A插入条件队列调用await唤醒B,但是在A唤醒后准备park时,B已经执行完需要的逻辑,并且再次Park,此时的A就可以跳过Park再次直接争夺锁。while (!isOnSyncQueue(node)) {// 3. 关键节点!!!:当前线程进入到等待状态LockSupport.park(this);if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)break;}// 4. 自旋等待获取到同步状态(即获取到lock)if (acquireQueued(node, savedState) && interruptMode != THROW_IE)interruptMode = REINTERRUPT;// 如果节点线程被取消才会进入这里的逻辑。正常不会if (node.nextWaiter != null) // clean up if cancelled//删除无效的等待节点unlinkCancelledWaiters();// 5. 处理被中断的情况if (interruptMode != 0)reportInterruptAfterWait(interruptMode);
}
// 添加新的条件队列节点
private Node addConditionWaiter() {Node t = lastWaiter;// 清除被取消的尾节点if (t != null && t.waitStatus != Node.CONDITION) {//解除关联unlinkCancelledWaiters();t = lastWaiter;}//将当前线程保存在Node中Node node = new Node(Thread.currentThread(), Node.CONDITION);if (t == null)firstWaiter = node;else//队尾插入t.nextWaiter = node;//更新lastWaiter (如果是第一次插入节点,头尾节点都是同一个)lastWaiter = node;return node;
}
//完全释放锁状态
final int fullyRelease(Node node) {boolean failed = true;try {int savedState = getState();// 这里会释放锁,并且唤醒后继节点if (release(savedState)) {//成功释放同步状态failed = false;return savedState;} else {//不成功释放同步状态抛出异常throw new IllegalMonitorStateException();}} finally {if (failed)node.waitStatus = Node.CANCELLED;}
}

Signal

  1. 检查本线程是否持有锁,正常是持有锁,如果不符合就抛出异常。

  2. 从等待队列中拿到第一个节点。如果头节点为空代表条件队列为空,谁也不通知直接结束。

  3. 将头节点从条件队列中移除,并且把nextWaiter置为null。然后把节点状态设为0,转移进入同步队列。如果队列为空则初始化同步队列。

  4. 如果前驱节点不是 signal 状态或者前一个节点已经被取消,直接对头节点线程解除阻塞。返回true跳出循环。

  5. 至此本线程方法执行结束。依旧持有锁,但是转移了条件队列的头节点到同步队列中,就做了这一件事。

//AQS
public final void signal() {//1. 先检测当前线程是否已经获取lockif (!isHeldExclusively())throw new IllegalMonitorStateException();//2. 获取等待队列中第一个节点,之后的操作都是针对这个节点Node first = firstWaiter;if (first != null)doSignal(first);
}//ReentrantLock
protected final boolean isHeldExclusively() {// While we must in general read state before owner,// we don't need to do so to check if current thread is ownerreturn getExclusiveOwnerThread() == Thread.currentThread();
}//AQS
private void doSignal(Node first) {do {if ( (firstWaiter = first.nextWaiter) == null)lastWaiter = null;//1. 将头结点从等待队列中移除first.nextWaiter = null;//2. while中transferForSignal方法对头结点做真正的处理} while (!transferForSignal(first) &&(first = firstWaiter) != null);
}final boolean transferForSignal(Node node) {//1. 更新状态为0if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))return false;//2.将该节点移入到同步队列中去// 这里的处理和同步队列的生成用的同一个方法// node p 为前驱节点(原尾节点)Node p = enq(node);int ws = p.waitStatus;// 如果前驱节点不是signal状态或者前一个节点已经被取消,直接对头节点解除阻塞。返回true跳出循环if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))LockSupport.unpark(node.thread);return true;
}

具体原理图如下:

feb70e6bbb0d750292ddad22c311e539.png

SignalAll

signalAll与signal方法的区别体现在doSignalAll方法上,前面我们已经知道doSignal方法只会对等待队列的头节点进行操作,而doSignalAll将条件队列中的所有Node都转移到了同步队列中,即“通知”当前调用condition.await()方法的每一个线程,代码如下。

private void doSignalAll(Node first) {lastWaiter = firstWaiter = null;do {Node next = first.nextWaiter;first.nextWaiter = null;transferForSignal(first);first = next;} while (first != null);
}

最后,欢迎大家提问和交流。

如果对你有帮助,欢迎点赞、评论或分享,感谢阅读!

update在MySQL中是怎样执行的,一张图牢记|原创

2022-11-19

eb697c182f74ee74d74b1d09d9bb5a23.jpeg

讲真,这篇最全HashMap你不能错过!|原创

2022-11-17

a96ce6f765bfa33ec4ee251352719e62.jpeg

MySQL主从数据不一致,怎么办?

2022-11-15

6e263aa5fa9758bc7a664efe60d69195.jpeg

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_226322.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

springboot+vue 任课教师考评评价系统 Java 前后端分离

随着我国教育改革的发展,各大高校的大学生数量也在不断的增加,相对应如何去评价教学的质量问题也是很多高校一直以来所关注的内容。只有有了一个好的教学评价制度才能够让各大高校的教学质量稳步提升,本系统就是这样一个根据各项指标对教学质…

(免费分享)基于ssm在线点餐

源码获取:关注文末gongzhonghao,017领取下载链接 开发工具:IDEA ,Tomcat8.0,数据库:mysql5.7 /*** FileName: CategoryController** Date: 2020/9/30 17:04* Description:*/ package com.qst.goldenarches.contro…

某大厂领导发邮件,怒斥员工“21点没人加班”,要求员工反思!

注意,又有奇葩领导出没。近日,有网友爆出恒生电子某领导发邮件“反思”21:00后没人上班,该领导说,当时自己脑子里冒出了几个念头:1.这些小组的工作任务都已经按时保质保量完成了吗?各项研发指标…

10.前端笔记-CSS-盒子模型-border和padding

页面布局的三大核心: 盒子模型浮动定位 1、盒子模型 1.1 盒子模型组成 盒子模型本质还是一个盒子,包括边框border、外边距margin、内边距padding和实际内容content 1.1.1 边框border 组成 组成:颜色border-color、边框宽度border-wid…

Spring项目结合Maven【实现读取不同的资源环境】

📃目录跳转📚简介:🍑修改pom.xml🥞修改application.yml🚀 演示:📚简介: 由于我们写功能的不能影响到线上环境的配置,所以每一次增加功能我们都要吧项目部署到…

JVM的垃圾回收机制(GC)

系列文章目录 JVM的内存区域划分_crazy_xieyi的博客-CSDN博客 JVM类加载(类加载过程、双亲委派模型)_crazy_xieyi的博客-CSDN博客 文章目录 一、什么是垃圾回收?二、java的垃圾回收,要回收的内存是哪些?三、回收堆上…

BGP服务器

BGP服务器被称为“边界网关协议”(BGP),是一种用于在不同主机网关、 Internet或自治系统之间传输数据和信息的路由协议。 BGP是一种路径矢量协议(PVP),它维护不同主机、网络和网关的路由器的路径,并根据 BGP做出路由决定。把电信、联通、联通…

Mac 使用paralles 从零搭建hadoop集群

目录 1. 虚机的安装与配置 1.1 安装parallels 1.2 安装fedora系统 1.3 fedora的配置 1.3.1 内存和硬盘配置 1.3.2 网络配置 1.3.3 共享文件夹 1.4 虚拟机克隆 与 加载 2. 免密登录 2.1 分别查看master, slave01,slave02 的ip 2.2 查看各虚机的…

Map学习笔记——深入理解ConcurrentHashMap

ConcurrentHashMap 是我们日常开发中使用频率最高的并发容器之一了,具有如下特点: 基于JDK8分析 存储结构和HashMap一样,都是数组 链表 红黑树是线程安全的容器,底层是通过CAS自旋 sychronized 来保证的key 和 value 都不允许为空&#xf…

异步请求-AJAX

什么是同步交互 首先用户向HTTP服务器提交一个处理请求。接着服务器端接收到请求后,按照预先编写好的程序中的业务逻辑进行处理,比如和数据库服务器进行数据信息交换。最后,服务器对请求进行响应,将结果返回给客户端,返…

黑苹果之微星(MSI)主板BIOS详细设置篇

很多童鞋安装黑苹果的时候会卡住,大部分原因是cfg lock 没有关闭,以及USB端口或SATA模式设置错误。 为了避免这些安装阶段报错的情况发生,今天给大家分享一下超详细的BIOS防踩坑设置指南--微星(MSI)主板BIOS篇&#xf…

CTFHub | Refer注入

0x00 前言 CTFHub 专注网络安全、信息安全、白帽子技术的在线学习,实训平台。提供优质的赛事及学习服务,拥有完善的题目环境及配套 writeup ,降低 CTF 学习入门门槛,快速帮助选手成长,跟随主流比赛潮流。 0x01 题目描述…

天然气潮流计算matlab程序

天然气潮流计算matlab程序 1 天然气潮流计算理论 由于天然气涉及到流体的运动方程,直接计算非常复杂,因此需要提前做出一些假设来简化计算,经过研究,适当的假设对结果影响很小,因此本文对天然气系统做出如下假设&#…

MNN--初步学习

来自阿里MNN有三个贡献点: 提出了预推理机制,在线计算推理成本和最优方案优化了kernel提出后端抽象实现混合调度MNN的架构: 分离线和在线两部分。离线就是很传统的模型转换、优化、压缩、量化的那一套东西,这里mnn转出的模型文件…

springboot thymeleaf使用

导入依赖 <dependency> <groupId>org.thymeleaf</groupId> <artifactId>thymeleaf</artifactId> <version>3.0.11.RELEASE</version> </dependency> <dependency> <groupId>org.thymeleaf</groupId> <a…

人口数据集:地级市常住人口与户籍人口、人口1%抽样调查数据两大维度指标数据

一、地级市常住人口与户籍人口 1、数据来源&#xff1a;地级市常住人口数据&#xff08;主要来源于各地政府公报&#xff09;&#xff0c;户籍人口数据来源于《中国城市统计年鉴》 2、时间跨度&#xff1a;2003-2019年 3、区域范围&#xff1a;280个地级市 4、指标说明&…

牛顿法(牛顿拉夫逊)配电网潮流计算matlab程序

牛顿法配电网潮流计算matlab程序 传统牛顿—拉夫逊算法&#xff0c;简称牛顿法&#xff0c;是将潮流计算方程组F(X)0&#xff0c;进行泰勒展开。因泰勒展开有许多高阶项&#xff0c;而高阶项级数部分对计算结果影响很小&#xff0c;当忽略一阶以上部分时&#xff0c;可以简化对…

校园论坛设计(Java)——介绍篇

校园论坛设计&#xff08;Java&#xff09; 文章目录校园论坛设计&#xff08;Java&#xff09;0、写在前面1、项目介绍2、项目背景3、项目功能介绍3.1 总体设计图3.2 帖子模块3.3 学习模块3.4 个人信息模块3.5 数据报表模块3.6 校园周边模块3.7 用户管理模块3.8 登录注册模块4…

[足式机器人]Part3机构运动微分几何学分析与综合Ch02-3 平面机构离散运动鞍点综合——【读书笔记】

本文仅供学习使用 本文参考&#xff1a; 《机构运动微分几何学分析与综合》-王德伦、汪伟 《微分几何》吴大任 Ch02-3 平面机构离散运动鞍点综合2.4 鞍滑点2.4.1 鞍线与二副连架杆P-R2.4.2 鞍线误差2.4.3 三位置鞍线2.4.4 四位置鞍线2.4.5 多位置鞍线2.4.6 滑点与鞍滑点2.4 鞍滑…

问题盘点|使用 Prometheus 监控 Kafka,我们该关注哪些指标

Kafka 作为当前广泛使用的中间件产品&#xff0c;承担了重要/核心业务数据流转&#xff0c;其稳定运行关乎整个业务系统可用性。本文旨在分享阿里云 Prometheus 在阿里云 Kafka 和自建 Kafka 的监控实践。01Kafka 简介Aliware01Kafka 是什么&#xff1f;Kafka 是分布式、高吞吐…