Java源码分析系列笔记-7.Lock之Condition
1. 是什么
类似object的wait和notify方法配合synchronized使用
condition的await和notify方法配合Lock使用,用来实现条件等待与唤醒
2. 如何使用
- 生产者消费者模式
public class ConditionTest
{
private Lock lock;//一个锁说明读写互斥
private int capacity;
private List<Object> items;
private Condition notFull;//用来唤醒写线程
private Condition notEmpty;//用来唤醒读线程
public ConditionTest(int capacity)
{
this.capacity = capacity;
this.items = new ArrayList<>();
this.lock = new ReentrantLock();
this.notFull = lock.newCondition();
this.notEmpty = lock.newCondition();
}
public void add(Object data) throws InterruptedException
{
try
{
lock.lock();
//新增的时候如果已经满了,那么等待 非满信号 唤醒
while (this.items.size() == capacity)
{
this.notFull.await();
}
//增加了一个元素,那么 唤醒非空
this.items.add(data);
this.notEmpty.signalAll();
}
finally
{
lock.unlock();
}
}
public Object remove() throws InterruptedException
{
try
{
lock.lock();
//删除的时候已经空了,那么等待 非空信号 唤醒
while (this.items.size() == 0)
{
this.notEmpty.await();
}
//删除了一个元素,那么 唤醒非满
Object data = this.items.remove(0);
this.notFull.signalAll();
return data;
}
finally
{
lock.unlock();
}
}
public static void main(String[] args)
{
ConditionTest conditionTest = new ConditionTest(5);
new Thread(() -> {
for (int i = 0; i < 1000; i++)
{
try
{
conditionTest.add(i);
System.out.println(String.format("生产者放入%d", i));
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
try
{
while (true)
{
Object data = conditionTest.remove();
System.out.println(String.format("消费者消费%d", data));
}
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}).start();
}
}
3. 实现原理
3.1. uml
3.2. 创建Condition对象
- newCondition方法
public Condition newCondition() {
//调用Sync的newCondition方法
return sync.newCondition();
}
3.2.1. 创建AQS.ConditionObject对象
- Sync newConditoin方法
final ConditionObject newCondition() {
//AQS的ConditionObject
return new ConditionObject();
}
3.2.1.1. ConditionObject内部也有一个双向队列
public class ConditionObject implements Condition, java.io.Serializable {
//condition队列也是一个双向队列
private transient Node firstWaiter;
private transient Node lastWaiter;
public ConditionObject() { }
}
结构如下图:
没错,Condition队列和AQS就是两个不同队列,Condition的操作就是在这两个队列中来回移动
3.3. await方法【阻塞等待】
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//加入condition队列尾部
Node node = addConditionWaiter();
//调用AQS解锁,释放互斥量(执行await肯定是在获取了锁后的)
int savedState = fullyRelease(node);
int interruptMode = 0;
//调用AQS死循环检测是否在AQS队列中,不在的话阻塞当前线程。
//什么时候加入AQS队列呢?signal的时候
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//获已经在AQS队列中了,获取锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
//如果node不是condition队列的尾节点
if (node.nextWaiter != null) // clean up if cancelled
//那么遍历删除conditoin队列中所有cancel节点
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
- 5行:加入condition队列尾部
- 7行:调用AQS解锁,释放互斥量(由此可知执行await肯定是在获取了锁后的)
- 11-15行:不停地检查是否在AQS阻塞队列中,不在的话阻塞当前线程。等待唤醒继续检查
- 17-22行:到达这里的时候说明已经在AQS队列中了,并且已被唤醒,那么我就要去抢占锁了。如果抢占失败继续回到11-15行
下面对这几个步骤作详细说明
3.3.1. 加入condition队列尾部
- addConditionWaiter
private Node addConditionWaiter() {
//队尾
Node t = lastWaiter;
//队尾的状态不为CONDITION(即为CANCEL)
if (t != null && t.waitStatus != Node.CONDITION) {
//删除conditoin队列中所有cancel节点
unlinkCancelledWaiters();
//重新从尾节点开始
t = lastWaiter;
}
//构造节点(当前线程,CONDITION状态)
Node node = new Node(Thread.currentThread(), Node.CONDITION);
//把节点加入condition队列尾部
//这是队列为空的情况
if (t == null)
firstWaiter = node;
//队列不为空的情况
else
t.nextWaiter = node;
lastWaiter = node;//新的尾节点
return node;
}
上面的代码所作的就是用当前线程构造成Condition节点,加入Condition队列的尾部;
除此之外,unlinkCancelledWaiters还会从头部开始往后删除conditoin队列中所有cancel节点,如下:
- unlinkCancelledWaiters方法
private void unlinkCancelledWaiters() {
//从头节点出发
Node t = firstWaiter;
Node trail = null;
//遍历condition队列
while (t != null) {
Node next = t.nextWaiter;
//如果节点状态为CANCEL
if (t.waitStatus != Node.CONDITION) {
//那么从condition队列中删除
t.nextWaiter = null;
//头节点是CANCEL的,那就修改头节点
if (trail == null)
firstWaiter = next;
//头节点不是CANCEL的,那就修改前一个节点的nextWaiter
else
trail.nextWaiter = next;
if (next == null)
lastWaiter = trail;
}
//节点状态不为CANCEL,那么跳过
else
trail = t;
//继续下一个节点
t = next;
}
}
3.3.2. 调用AQS解锁,释放互斥量
- AQS fullyRelease方法
final int fullyRelease(Node node) {
boolean failed = true;
try {
//获取当前互斥量
int savedState = getState();
//调用AQS.release释放这些互斥量
if (release(savedState)) {
//释放成功后返回释放的互斥量个数
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
//解锁失败需要把当前节点置为CANCEL状态
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
- AQS release
public final boolean release(int arg) {
//调用AQS.tryRelease释放锁
if (tryRelease(arg)) {
//释放锁成功后把AQS队列的头节点的线程唤醒
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
由于ReentrantLock重写的了AQS的tryRelease,因此调用的是ReentrantLock.tryRelease,如下:
3.3.2.1. 尝试释放互斥量
- ReentrantLock.tryRelease
protected final boolean tryRelease(int releases) {
//计算释放完releases个信号量还剩多少要释放
int c = getState() - releases;
//解锁的必须和加锁同一线程
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {//剩余0个说明解锁成功
free = true;
setExclusiveOwnerThread(null);//置持有锁的线程为空
}
//设置剩余的信号量
//由于解锁的只有一个线程,所以这里不需要使用CAS操作设置state
setState(c);
return free;
}
3.3.3. 检测是否在AQS队列,不在则需要阻塞
- isOnSyncQueue方法
final boolean isOnSyncQueue(Node node) {
//node的状态是CONDITION,说明还在condition队列中 或者 前一个节点为空
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;//返回false,表示不在AQS队列中
//next不为空(next是AQS队列专用,nextWaiter是Condition队列专用),一定在AQS队列中
if (node.next != null) // If has successor, it must be on queue
return true;//返回true,表示在AQS队列中
//以上两种情况都不符合,那么只能到AQS队列中查找
return findNodeFromTail(node);
}
private boolean findNodeFromTail(Node node) {
Node t = tail;
//从尾开始遍历,找到node
for (;;) {
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
}
3.3.4. 当前节点已经在AQS队列中了,获取锁
- acquireQueue
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//死循环直到获取锁成功
for (;;) {
//逻辑1.
//当前节点的前一个节点时头节点的时候(公平锁:即我的前面没有人等待获取锁),尝试获取锁
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
//获取锁成功后设置头节点为当前节点
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//逻辑2.
//当前节点的前一个节点状态时SIGNAL(承诺唤醒当前节点)的时候,阻塞当前线程。
//什么时候唤醒?释放锁的时候
//唤醒之后干什么?继续死循环执行上面的逻辑1
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//如果发生了异常,那么执行下面的cancelAcquire方法
if (failed)
cancelAcquire(node);
}
}
3.3.4.1. 判断是否需要阻塞
- shouldParkAfterFailedAcquire
//根据(前一个节点,当前节点)->是否阻塞当前线程
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
//前一个节点的状态时SIGNAL,即释放锁后承诺唤醒当前节点,那么返回true可以阻塞当前线程
if (ws == Node.SIGNAL)
return true;
//前一个节点状态>0,即CANCEL。
//那么往前遍历找到没有取消的前置节点。同时从链表中移除CANCEL状态的节点
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
// 前置节点状态>=0,即0或者propagate。
//这里通过CAS把前置节点状态改成signal成功获取锁,失败的话再阻塞。why?
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
3.3.4.1.1. 阻塞当前线程
- parkAndCheckInterrupt
private final boolean parkAndCheckInterrupt() {
//使用Unsafe阻塞当前线程,这里会清除线程中断的标记,因此需要返回中断的标记
LockSupport.park(this);
return Thread.interrupted();
}
3.4. signalAll方法【唤醒所有阻塞等待的节点】
- ConditionObject signalAll
public final void signalAll() {
//如果当前线程不是持有互斥量的线程,直接抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//Condition队列不为空
Node first = firstWaiter;
if (first != null)
//把condition队列的所有节点转移到AQS队列中并唤醒所有线程
doSignalAll(first);
}
3.4.1. 把condition队列的所有节点转移到AQS队列中
- doSignalAll方法
private void doSignalAll(Node first) {
//清空condition队列的头、尾节点
lastWaiter = firstWaiter = null;
//遍历condition队列
do {
Node next = first.nextWaiter;
first.nextWaiter = null;
//转移到AQS队列中
transferForSignal(first);
first = next;
} while (first != null);
}
3.4.1.1. 每转移一个condition队列中的节点到aqs队列中,就唤醒一个
- tansferForSignal方法
final boolean transferForSignal(Node node) {
//当前节点是CONDITION状态,CAS设置为0,如果成功继续15行
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;//CAS设置失败,那么返回false表示唤醒失败
//调用AQS enq,把当前节点加入AQS队列
Node p = enq(node);
int ws = p.waitStatus;
//如果该结点的状态为cancel 或者 修改waitStatus为SIGNAL失败
//没搞懂这个条件什么意思
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
//唤醒当前节点的线程
LockSupport.unpark(node.thread);
return true;
}
3.4.1.1.1. 如何转移的
- AQS.enq
private Node enq(final Node node) {
//死循环直到加入队尾成功
for (;;) {
Node t = tail;
//队列为空初始化头节点(占位符)
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else {//加入队尾
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
3.5. signal方法【只唤醒头部阻塞等待的节点】
- ConditionObject signal
public final void signal() {
//调用ReentrantLock的方法判断当前线程是否持有锁的线程
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//condition队列不为空
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
3.5.1. 唤醒头节点
- doSignal
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)//修改头结点,完成旧头结点的移出工作
lastWaiter = null;
first.nextWaiter = null;
//将老的头结点,加入到AQS的等待队列中
//一旦成功唤醒一个,那么退出循环返回(signalAll是唤醒所有)
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
4. 为什么await需要先释放锁,而signal不需要
因为await可能需要阻塞,所以在阻塞前需要先释放锁。
5. 总结
condition的一系列操作其实只涉及了AQS队列和condition队列的来回移动
- 当执行await方法时,会把当前线程加入到condition队列中,然后释放锁。接着不断检查是否在AQS队列中。
是的话开始竞争锁,只有AQS队列中的首节点能抢占成功。否则挂起 - 当执行signalAll方法时,会把condition队列中所有节点转移到AQS队列中,并唤醒所有线程。被唤醒的节点会退出是否在AQS队列中的检查,开始抢占锁
- 当执行signal方法时,会把condition队列中头节点转移到AQS队列中,并唤醒该头节点的线程。被唤醒的节点会退出是否在AQS队列中的检查,开始抢占锁
6. 参考链接
Java源码分析系列笔记-7.Lock之Condition的更多相关文章
- Java源码分析系列之HttpServletRequest源码分析
从源码当中 我们可以 得知,HttpServletRequest其实 实际上 并 不是一个类,它只是一个标准,一个 接口而已,它的 父类是ServletRequest. 认证方式 public int ...
- Java源码分析系列
1) 深入Java集合学习系列:HashMap的实现原理 2) 深入Java集合学习系列:LinkedHashMap的实现原理 3) 深入Java集合学习系列:HashSet的实现原理 4) 深入Ja ...
- MyCat源码分析系列之——结果合并
更多MyCat源码分析,请戳MyCat源码分析系列 结果合并 在SQL下发流程和前后端验证流程中介绍过,通过用户验证的后端连接绑定的NIOHandler是MySQLConnectionHandler实 ...
- Spring Ioc源码分析系列--Bean实例化过程(一)
Spring Ioc源码分析系列--Bean实例化过程(一) 前言 上一篇文章Spring Ioc源码分析系列--Ioc容器注册BeanPostProcessor后置处理器以及事件消息处理已经完成了对 ...
- MyCat源码分析系列之——SQL下发
更多MyCat源码分析,请戳MyCat源码分析系列 SQL下发 SQL下发指的是MyCat将解析并改造完成的SQL语句依次发送至相应的MySQL节点(datanode)的过程,该执行过程由NonBlo ...
- MyCat源码分析系列之——BufferPool与缓存机制
更多MyCat源码分析,请戳MyCat源码分析系列 BufferPool MyCat的缓冲区采用的是java.nio.ByteBuffer,由BufferPool类统一管理,相关的设置在SystemC ...
- [Tomcat 源码分析系列] (二) : Tomcat 启动脚本-catalina.bat
概述 Tomcat 的三个最重要的启动脚本: startup.bat catalina.bat setclasspath.bat 上一篇咱们分析了 startup.bat 脚本 这一篇咱们来分析 ca ...
- MyBatis 源码分析系列文章导读
1.本文速览 本篇文章是我为接下来的 MyBatis 源码分析系列文章写的一个导读文章.本篇文章从 MyBatis 是什么(what),为什么要使用(why),以及如何使用(how)等三个角度进行了说 ...
- spring源码分析系列 (8) FactoryBean工厂类机制
更多文章点击--spring源码分析系列 1.FactoryBean设计目的以及使用 2.FactoryBean工厂类机制运行机制分析 1.FactoryBean设计目的以及使用 FactoryBea ...
- spring源码分析系列 (5) spring BeanFactoryPostProcessor拓展类PropertyPlaceholderConfigurer、PropertySourcesPlaceholderConfigurer解析
更多文章点击--spring源码分析系列 主要分析内容: 1.拓展类简述: 拓展类使用demo和自定义替换符号 2.继承图UML解析和源码分析 (源码基于spring 5.1.3.RELEASE分析) ...
随机推荐
- C# 13 中的新增功能实操
前言 今天大姚带领大家一起来看看 C# 13 中的新增几大功能,并了解其功能特性和实际应用场景. 前提准备 要体验 C# 13 新增的功能可以使用最新的 Visual Studio 2022 版本或 ...
- 多态的转型和案例--java进阶day02
1.多态的转型 1.向上转型 我们之前学的多态创建对象,使用的都是向上转型,父类引用指向子类(赋值方式则是从子到父),f拿到子类的地址,就能访问子类的堆内存 2.向下转型 和向上转型相反,子类引用指向 ...
- 《机器人SLAM导航核心技术与实战》第1季:第11章_自主导航中的数学基础
<机器人SLAM导航核心技术与实战>第1季:第11章_自主导航中的数学基础 视频讲解 [第1季]11.第11章_自主导航中的数学基础-视频讲解 [第1季]11.1.第11章_自主导航中的数 ...
- study Rust-3【表达式和函数】
1. Rust与优美的pascal很相似.但是这个表达式概念很有意思.见上图.[1.条件赋值语句:2.表达式返回值] 2.注意变量和隐藏变量的概念,这个也有创意. 3.函数在Rust无处不在.
- 主存的扩展及其CPU的连接——位扩展
其初始状态 进行读操作: 输入对应地址,将MREQ端设置为低电平,此时片选端有效,r/w端为高电平,所以写使能端无效,然后通过数据线和数据总线,CPU读取数据. 进行写操作: 输入对应地址,将R/W设 ...
- 二叉树 (王道数据结构 C语言版)
2004.11.04 计算一颗给定二叉树的所有双分支节点个数 编写把一个树的所有左右子树进行交换的函数 求先序遍历中第k个结点的值 (1 <= k <= 二叉树中的结点个数) #inclu ...
- 应用引入LLM实践
LLM最近在各行各业遍地开花,产生了很好的效果,也落地了很多好的功能应用. 无论是从实际应用角度,还是从营销角度,我们都需要接入大模型能力. 拿国内比较火的Deepseek来说,具有良好的推理能力,可 ...
- redis的作用:高性能和高并发
一.高性能 假设这么个场景,你有个操作,一个请求过来,吭哧吭哧你各种乱七八糟操作mysql,半天查出来一个结果,耗时600ms.但是这个结果可能接下来几个小时都不会变了,或者变了也可以不用立即反馈给用 ...
- Sentinel源码—7.参数限流和注解的实现
大纲 1.参数限流的原理和源码 2.@SentinelResource注解的使用和实现 1.参数限流的原理和源码 (1)参数限流规则ParamFlowRule的配置Demo (2)ParamFlowS ...
- 解决微信二维码接口接口返回:errcode\":47001,\"errmsg\":\"data format error rid: xxx和处理返回的buffer的问题
data format error rid问题: 在php中使用curl调用微信二维码生成接口getwxacodeunlimit时得到错误响应信息: errcode\":47001,\&qu ...