Condition的await()和signal()流程
介绍
Condition
是j.u.c
包下提供的一个接口。
可以翻译成 条件对象,其作用是线程先等待,当外部满足某一条件时,在通过条件对象唤醒等待的线程。ArrayBlockingQueue
就是通过Condition
实现的。
先看一下Condition
接口提供了哪些方法:
/**
* 条件对象
*/
public interface Condition {
/**
* 让线程进入等待,如果其他线程调用同一Condition对象的notify/notifyAll,那么等待的线程可能被唤醒
*/
void await() throws InterruptedException;
/**
* 不抛出中断异常的await方法
*/
void awaitUninterruptibly();
/**
* 带超时的await
*/
long awaitNanos(long nanosTimeout) throws InterruptedException;
/**
* 带超时的await(可指定时间单位)
*/
boolean await(long time, TimeUnit unit) throws InterruptedException;
/**
* 带超时的await(指定截止时间)
*/
boolean awaitUntil(Date deadline) throws InterruptedException;
/**
* 唤醒等待的线程
*/
void signal();
/**
* 唤醒所有线程
*/
void signalAll();
}
Condition
接口主要提供了两类方法——让线程等待的方法(await()等)和唤醒线程的方法(signal())。
AQS
内部提供了Condition
接口的实现——ConditionalObject
。它内部的字段如下:
private static final long serialVersionUID = 1173984872572414699L;
//该ConditionObject维护的等待队列的头节点
private transient Node firstWaiter;
//该ConditionObject维护的等待队列的尾节点
private transient Node lastWaiter;
非常简单,从上面的字段我们大概可以猜到Condition
内部也维护了一个队列。
上篇文章中,我们已经分析锁实现的远离就是通过节点构成队列:让队列中除头节点外的其他线程都被Park,当头节点释放锁时,头节点唤醒下一个节点(Unpark线程),同时更新头节点。
举一反三,我们推测Condition
唤醒功能的原理也是通过维护队列的节点。
接下来就通过分析源码,(主要是await()
和signal()
方法),验证我们的猜测。
Condtion对象的获取
Condition
对象的获取主要是通过Lock.newCondition()
方法。
一个Lock
对象可以返回多个Condition
对象。
在对Condition
进行等待或者唤醒前,都需要先持有Condition
关联Lock
对象,否则会抛出IllegalMonitorStateException
异常。
Condition.await()过程
public final void await() throws InterruptedException {
//如果线程已经被标记为中断,则抛出异常
if (Thread.interrupted())
throw new InterruptedException();
//将线程添加进等待队列
//注意等待队列和AQS维护的阻塞队列是两个不同的队列
//正常流程当线程能调用await(),说明线程此时拥有锁,此时AQS的阻塞队列中,线程应该在head节点
Node node = addConditionWaiter();
//释放掉锁(如果释放失败,NODE的waitStatus被更新为CANCELLED)
//同时因为释放掉了锁,该线程在阻塞队列中的节点也已经被移除
int savedState = fullyRelease(node);
//这里会将线程挂起,除非线程节点被移到AQS的阻塞队列或是线程被外部中断
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
//检查是否是由于被中断而唤醒,如果是,则跳出循环
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//在阻塞队列中尝试获取锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
//节点已经在阻塞队列中,与Condition的等待队列联系断开
//对于SIGNAL唤醒的线程而言,SIGNAL时除了将节点移到阻塞队列,同时也清空了node.nextWaiter
//而对于中断唤醒的线程而言,只是将节点移到阻塞队列,并没有清空node.nextWaiter(因为此时线程不持有,操作等待线程并非线程安全)
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
//根据interruptMode 决定是否需要抛出异常
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
await()
方法主要可以分为以下几个过程:
1)线程将自己包装成节点,并添加到Condition
的阻塞队列中
2)线程主动释放掉锁
3)线程进入自循环等待(主动通过LockSupport.park()
),醒来时,检查自己是否已经被移动至Lock的阻塞队列
4)线程在阻塞队列中等待,直到获取锁(线程在等待时可能又会被LockSupport.park()
挂起)
5)线程获取锁,检查自己在等待过程中(await()
过程)是否有被中断
6)如果有需要,则清理节点与等待队列之间的联系
7)根据中断状态确定是否需要抛出异常,以便让await()
的调用者可以响应线程的中断状态
从上面的流程,我们可以清楚的了解到以上步骤1和2,以及步骤5,6,7是持有锁的,步骤3和4并没有持有锁。了解这一点很重要,因为涉及某些方法是否需要以CAS来保证线程安全。
了解了大体流程,接下来就逐步分析各个步骤。
步骤1.线程包装成节点,添加进Condition
的等待队列
这一步骤主要是addConditionWaiter()
过程
private Node addConditionWaiter() {
Node t = lastWaiter;
// 找出该ConditionObject的等待队列中 真正未被取消的最后一个节点,并更新为lastWaiter
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
//将该线程包装成Node
Node node = new Node(Thread.currentThread(), Node.CONDITION);
//如果此时ConditionObject队列为空,初始化链表且头节点为node
if (t == null)
firstWaiter = node;
else //否则将node添加队尾
t.nextWaiter = node;
//更新链表尾节点为node
lastWaiter = node;
return node;
}
主要找出等待队列的最后一个节点,将线程包装成Node,添加到队列的队尾。
这里要注意的一点是此时Node的waitStatus
为CONDITION
。节点的waitStatus
对判断等待是否被取消很重要,在等待队列中等待的节点状态应该为CONDITION
,如果状态不为CONDITION
,说明线程已经取消了等待(如果waitStatus为0说明被唤醒或中断)。
步骤2.线程释放锁
释放锁的步骤比较简单。主要通过fullRelease()
更新AQS
的state
为0并且将AQS
的拥有者置为null,同时唤醒阻塞队列中的后继节点。
final int fullyRelease(Node node) {
boolean failed = true;
try {
//获取锁被持有的此时
int savedState = getState();
//让锁直接释放被持有的次数
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
//如果释放失败了,则将节点标记为CANCELLED
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
步骤3.线程挂起,进入循环等待
这一步比较关键,线程等待的动作都发生在这一步。
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
//检查是否是由于被中断而唤醒,如果是,则跳出循环
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
线程执行到这,会被LockSupport.park()
挂起。
如果此时线程被唤醒,线程会检查是否是因为中断,如果发生中断,还需要确定中断是否发生在SIGNAL前(如果发生在SIGNAL前,之后线程需要抛出异常,让外部响应)。
如果线程不是因为中断而唤醒,线程需要确认节点是否已经被移动至AQS
的等待队列。如果没有被移动,则继续被挂起(防止假唤醒)。
checkInterruptWhileWaiting()
就是用来检测线程在等待的时候是否被中断。
/**
* 检查线程在WAITING状态期间,是否有被中断
* 如果没有返回0;如果是在SIGNAL之前被中断,返回 THROW_IE;如果在SIGNAL之后被中断,则返回REINTERRUPT
*/
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
如果线程被中断,还需要通过transferAferCancelledWait
判断中断是否发生在SIGNAL
之前。
final boolean transferAfterCancelledWait(Node node) {
//CAS操作,期待值是CONDITION,说明此时唤醒是被取消(中断),因为如果是SIGNAL,那么waitingStatus不会CONDITION,而是0(可以见SIGNAL流程)
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
//即使是取消的,也需要移到AQS的阻塞队列
enq(node);
return true;
}
//说明线程先收到了SIGNAL信号
//此时要等SIGNAL信号处理完成
while (!isOnSyncQueue(node))
Thread.yield();
return false;
}
前文说了Node节点的waitStatus
是一个很重要的状态,从它可以推断出线程节点发生了什么操作。
上述方法先用CAS
尝试将更新节点的waitStatus
为0,期待值为CONDITION
。
如果尝试成功,说明此时节点未被操作过(SIGNAL信号),线程是中断唤醒的,此时需要通过enq()
将节点添加AQS
阻塞队列,因为此时没有锁,所以enq()
方法以CAS重试的方式保证线程安全。
如果尝试失败,说明线程收到了SIGNAL
信号,节点将由负责SIGNAL
的线程移动至阻塞队列。这里为了避免线程返回过早,在判断出线程还未移动至阻塞队列的情况下,会通过Thread.yeild()
让出CPU时间。
步骤4.线程在阻塞队列中重新等待锁
这一步主要是通过acquireQueue()
方法。该方法已经在上一篇文章中介绍过了,这里不过多介绍。
需要注意的一点是,即使线程在等待时被中断,仍然需要在AQS
的阻塞队列中等待获取锁。因为外部没有办法在线程获取锁之前发现中断状态,而且即使线程抛出了中断异常,此时线程也是持有锁的,外部需要显式的释放。
步骤5.检查并设置中断状态
这一步很简单,主要就是通过步骤3中的checkInterruptWhilewaiting()
方法返回值:0表示未中断,-1表示中断发生在SIGNAl之前,1表示中断发生在SIGNAL之后。
步骤6.清理节点与等待队列的联系
这里有两种情况,如果线程是因为SIGNAL
唤醒的,在唤醒时调用signal()
的线程已经清理了被唤醒节点与等待队列的关系。 因为那时唤醒线程持有锁,操作是安全的。
但是如果对于中断被唤醒的线程,唤醒时是不持有锁的,不能保证线程安全的清理唤醒节点与等待队列的关系。因此就将等待清理工作放在了获取锁之后。
//此方法并不保证线程安全,因此调用此方法时,必须要在获取锁的情况下调用
//此方法的目的是为了整理Condition的等待队列,将非CONDITION状态的节点从等待队列中移除
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
Node trail = null;
while (t != null) {
Node next = t.nextWaiter;
if (t.waitStatus != Node.CONDITION) {
t.nextWaiter = null;
if (trail == null)
firstWaiter = next;
else
trail.nextWaiter = next;
if (next == null)
lastWaiter = trail;
}
else
trail = t;
t = next;
}
}
步骤7.确定中断状态,并决定是否抛出异常
这一步也很简单,就是根据interruptMode
来确定是否抛出异常,如果interruptMode
值为THROW_IE,说明线程被唤醒前先被中断,此时需要抛出InterruptedException
。
Condition.signal()过程
signal()
过程比起await()
要简单很多。既然await()
过程是将节点添加到等待队列,那么signal()
作为await()
的逆过程,就是将节点从等待队列重新移动到AQS
阻塞队列。
/**
* 唤醒等待节点
* 主要的流程就是将节点从Condition的等待队列移到AQS的阻塞队列中,让其重新等待锁的获取
*/
public final void signal() {
//先验证唤醒者是否是锁的持有者
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//唤醒等待队列的第一个节点
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
为保证安全,先确定唤醒线程是否为锁的持有者。
之后找出等待队列中的头节点,将其唤醒。
/**
* 唤醒操作(此时占有锁,为线程安全),
* 找出等待队列中第一个真正要被唤醒的节点,移动到阻塞队列,
*/
private void doSignal(Node first) {
do {
//找出第一个需要唤醒的节点
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
//断开和之后等待队列的联系,并更新等待队列的头节点
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null); //等待队列的头节点移动失败 且 等待队列中还有其他节点,则继续尝试其他节点
}
因为头节点可能已经被取消,所以这里会一直在等待队列中从前往后找一个节点,开始唤醒。直到一个节点唤醒成功或者等待队列中没有节点需要唤醒。
上文也说了唤醒过程其实就是节点的移动过程。
/**
* 将节点从Condition的等待队列移动到AQS的阻塞队列(该方法只会在signal相关的方法中被调用)
*/
final boolean transferForSignal(Node node) {
//CAS操作更新节点的waitStatus,期待值为CONDITION
//操作成功,说明没有其他线程在操作这个节点
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) //CAS操作失败,说明节点已经被中断操作过,waitStatus已经变成了0
return false;
//这里说明CAS操作成功
//应该是线程安全的
//将节点添加到阻塞队列的队尾
Node p = enq(node);
//判断此时阻塞队列是否有前继节点等待,有就Park线程,等待前继节点唤醒
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
transferForSignal()
操作其实就是先通过CAS
操作确定等待队列中的节点还未被取消。如果CAS
操作成功,则将其添加到阻塞队列,如果此时阻塞队列还需要等待锁,则让await()
的线程继续被挂起。将给线程唤醒的任务(此时已经是获取锁的任务,不再是之前的await()任务)交给阻塞队列中的前继节点。
await()以及signal()过程的流程图
跟着流程图在走一遍,可以帮助巩固上述的知识点。
总结
再借上篇文章重口味的比方梳理下这个流程。
当你排队进了WC的包厢,想要方便时,你觉得太脏了,于是你在包厢内留了张纸条,希望有人能在厕所包厢干净了在叫你过来上厕所(调用Condition的await ()),随后主动让出了包厢的使用权(释放锁)。后面在排队等着方便的人便进去了。
你离开后,到另一个地方边玩手机边等(移动到Condition的阻塞队列,并被Park),期间,也有一些人同样觉得厕所太脏,跑了出来,在外面等,并排在了你的后面。
过了一段时间,有人把打电话给你说厕所已经变干净了,可以去用了,你就重新回到厕所那排起了队伍,等待轮到你用厕所(acquireQueue)。因为那个人仅通知了你,没有通知其他因为嫌弃厕所脏,而跑出来的人,所以那些只能继续在那等别人去叫他们。
如果你在等待的时候突然有人用其他方式把你喊了回去(外部未给signal却被中断),你又主动从在外面等待走到了厕所前去排队等待继续去用厕所,但是等你重新排到包厢时,发现测试还是很脏(Condition的 条件未满足),你就会抛出异常。
Condition的await()和signal()流程的更多相关文章
- object的wait()、notify()、notifyAll()、方法和Condition的await()、signal()方法
wait().notify()和notifyAll()是 Object类 中的方法 从这三个方法的文字描述可以知道以下几点信息: 1)wait().notify()和notifyAll()方法是本地方 ...
- 12.详解Condition的await和signal等待通知机制
1.Condition简介 任何一个java对象都天然继承于Object类,在线程间实现通信的往往会应用到Object的几个方法,比如wait(),wait(long timeout),wait(lo ...
- Java并发编程,Condition的await和signal等待通知机制
Condition简介 Object类是Java中所有类的父类, 在线程间实现通信的往往会应用到Object的几个方法: wait(),wait(long timeout),wait(long tim ...
- 详解Condition的await和signal等待/通知机制
本人免费整理了Java高级资料,涵盖了Java.Redis.MongoDB.MySQL.Zookeeper.Spring Cloud.Dubbo高并发分布式等教程,一共30G,需要自己领取.传送门:h ...
- 使用Condition配合await()和signal()实现等待/通知
关键字Synchronized与wait()和notify()/notifyAll()结合可以实现“等待/通知”模式, Lock类的子类ReentrantLock也可以实现同样的功能,但需要借助Con ...
- Java并发包源码学习系列:详解Condition条件队列、signal和await
目录 Condition接口 AQS条件变量的支持之ConditionObject内部类 回顾AQS中的Node void await() 添加到条件队列 Node addConditionWaite ...
- 生产者与消费者(二)---await与 signal
前面阐述了实现生产者与消费者问题的一种方式:wait() / notify()方法,本文继续阐述多线程的经典问题---生产者与消费者的第二种方式:await() / signal()方法. await ...
- Lock的lock/unlock, condition的await/singal 和 Object的wait/notify 的区别
在使用Lock之前,我们都使用Object 的wait和notify实现同步的.举例来说,一个producer和consumer,consumer发现没有东西了,等待,produer生成东西了,唤醒. ...
- “全栈2019”Java多线程第三十三章:await与signal/signalAll
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...
随机推荐
- ubuntu16.04下安装java8
从Oracle官网下载jdk,jdk-8u231-linux-x64.tar.gz (1)复制到/opt 目录内 sudo cp jdk-8u231-linux-x64.tar.gz /opt (3) ...
- For,while,case,shell循环结构
For,while,case,shell循环结构 案例1:使用for循环结构 案 ...
- 一天学一个Linux命令:第一天 ls
文章更新于:2020-03-02 注:本文参照 man ls 手册,并给出使用样例. 文章目录 一.命令之`ls` 1.名字及介绍 2.语法格式 3.输出内容示例 4.参数 二.命令实践 1.`ls ...
- git rebase解决合并冲突
git rebase解决合并冲突 记录合并冲突解决方法,使用的git rebase,感觉很好用 1.git rebase 文档 https://git-scm.com/docs/git-rebas ...
- 汇编刷题:显示ABCDEFGH 八个字母
DATA SEGMENT DATA ENDS CODE SEGMENT ASSUME CS:CODE,DS:DATA START: MOV AX,DATA MOV DS,AX MOV CX,8 MOV ...
- Jenkins构建项目后发送钉钉消息推送
前言 钉钉是我们日常工作的沟通工具,在Jenkins构建持续集成项目配合钉钉机器人的功能,可以让我们在持续集成测试环节快速接收到测试结果的消息推送. 一:新建一个钉钉群,选择自定义机器人 二:添加机器 ...
- 条件变量 condition_variable wait_for
wait_for(阻塞当前线程,直到条件变量被唤醒,或到指定时限时长后) #include <iostream> #include <atomic> #include < ...
- CVE-2020-1938:Apache-Tomcat-Ajp漏洞-复现
0x00 漏洞简介 Apache与Tomcat都是Apache开源组织开发的用于处理HTTP服务的项目,两者都是免费的,都可以做为独立的Web服务器运行. Apache Tomcat服务器存在文件包含 ...
- 讲讲HashMap的理解,以及HashMap在1.7和1.8版本的变化(2020/4/16)
HashMap的适用场景,作用,优缺点
- AJ学IOS(36)UI之手势事件旋转_缩放_拖拽
AJ分享,必须精品 效果 完成一个图片的捏合缩放,拖拽,旋转动作. 设计思路 拖拽: 首先是最简单的拖拽 //拖拽 -(void)panTest { UIPanGestureRecognizer *p ...