17.AQS中的Condition是什么?
欢迎关注:王有志
期待你加入Java人的提桶跑路群:共同富裕的Java人
今天来和大家聊聊Condition
,Condition
为AQS“家族”提供了等待与唤醒的能力,使AQS"家族"具备了像synchronized
一样暂停与唤醒线程的能力。我们先来看两道关于Condition
的面试题目:
Condition
和Object
的等待与唤醒有什么区别?- 什么是
Condition
队列?
接下来,我们就按照“是什么”,“怎么用”和“如何实现”的顺序来揭开Condition
的面纱吧。
Condition是什么?
Condition
是Java中的接口,提供了与Object#wait
和Object#notify
相同的功能。Doug Lea在Condition
接口的描述中提到了这点:
Conditions (also known as condition queues or condition variables) provide a means for one thread to suspend execution (to "wait") until notified by another thread that some state condition may now be true.
来看Condition
接口中提供了哪些方法:
public interface Condition {
void await() throws InterruptedException;
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
void signal();
void signalAll();
}
Condition
只提供了两个功能:等待(await)和唤醒(signal),与Object
提供的等待与唤醒时相似的:
public final void wait() throws InterruptedException;
public final void wait(long timeoutMillis, int nanos) throws InterruptedException;
public final native void wait(long timeoutMillis) throws InterruptedException;
@HotSpotIntrinsicCandidate
public final native void notify();
@HotSpotIntrinsicCandidate
public final native void notifyAll();
唤醒功能上,Condition
与Object
的差异并不大:
Condition#signal
\(\approx\)Object#notify
Condition#signalAll
\(=\)Object#notifyAll
多个线程处于等待状态时,Object#notify()
是“随机”唤醒线程,而Condition#signal
则由具体实现决定如何唤醒线程,如:ConditionObject
唤醒的是最早进入等待的线程,但两个方法均只唤醒一个线程。
等待功能上,Condition
与Object
的共同点是:都会释放持有的资源,Condition
释放锁,Object
释放Monitor,即进入等待状态后允许其他线程获取锁/监视器。主要的差异体现在Condition
支持了更加丰富的场景,通过一张表格来对比下:
Condition 方法 |
Object 方法 |
解释 |
---|---|---|
Condition#await() |
Object#wait() |
暂停线程,抛出线程中断异常 |
Condition#awaitUninterruptibly() |
/ | 暂停线程,不抛出线程中断异常 |
Condition#await(time, unit) |
Object#wait(timeoutMillis, nanos) |
暂停线程,直到被唤醒或等待指定时间后,超时后自动唤醒返回false,否则返回true |
Condition#awaitUntil(deadline) |
/ | 暂停线程,直到被唤醒或到达指定时间点,超时后自动唤醒返回false,否则返回true |
Condition#awaitNanos(nanosTimeout) |
/ | 暂停线程,直到被唤醒或等待指定时间后,返回值表示被唤醒时的剩余时间(nanosTimeout-耗时),结果为负数表示超时 |
除了以上差异外,Condition
还支持创建多个等待队列,即同一把锁拥有多个等待队列,线程在不同队列中等待,而Object
只有一个等待队列。《Java并发编程的艺术》中也有一张类似的表格,放在这里供大家参考:
Tips:
- 实际上signal翻译为唤醒并不恰当~~
- 涉及到
Condition
的实现部分,下文通过AQS中的ConditionObject
详细解释。
Condition怎么用?
既然Condition
与Object
提供的等待与唤醒功能相同,那么它们的用法是不是也很相似呢?
与调用Object#wait
和Object#notifyAll
必须处于synchronized
修饰的代码中一样(获取Monitor),调用Condition#await
和Condition#signalAll
的前提是要先获取锁。但不同的是,使用Condition
前,需要先通过锁去创建Condition
。
以ReentrantLock
中提供的Condition
为例,首先是创建Condition
对象:
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
然后是获取锁并调用await
方法:
new Thread(() -> {
lock.lock();
try {
condition.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
lock.unlock();
}
最后,通过调用singalAll
唤醒全部阻塞中的线程:
new Thread(() -> {
lock.lock();
condition.signalAll();
lock.unlock();
}
ConditionObject的源码分析
作为接口Condition
非常惨,因为在Java中只有AQS中的内部类ConditionObject
实现了Condition
接口:
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
public class ConditionObject implements Condition, java.io.Serializable {
private transient Node firstWaiter;
private transient Node lastWaiter;
}
static final class Node {
// 省略
}
}
ConditionObject
只有两个Node
类型的字段,分别是链式结构中的头尾节点,ConditionObject
就是通过它们实现的等待队列。那么ConditionObject
的等待队列起到了怎样的作用呢?是类似于AQS中的排队机制吗?带着这两个问题,我们正是开始源码的分析。
await方法的实现
Condition
接口中定义了4个线程等待的方法:
void await() throws InterruptedException
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
方法虽然很多,但它们之间的差异较小,只体现在时间的处理上,我们看其中最常用的方法:
public final void await() throws InterruptedException {
// 线程中断,抛出异常
if (Thread.interrupted()) {
throw new InterruptedException();
}
// 注释1:加入到Condition的等待队列中
Node node = addConditionWaiter();
// 注释2:释放持有锁(调用AQS的release)
int savedState = fullyRelease(node);
int interruptMode = 0;
// 注释3:判断是否在AQS的等待队列中
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
// 中断时退出方法
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) {
break;
}
}
// 加入到AQS的等待队列中,调用AQS的acquireQueued方法
if (acquireQueued(node, savedState) && interruptMode != THROW_IE) {
interruptMode = REINTERRUPT;
}
// 断开与Condition队列的联系
if (node.nextWaiter != null) {
unlinkCancelledWaiters();
}
if (interruptMode != 0) {
reportInterruptAfterWait(interruptMode);
}
}
注释1的部分,调用addConditionWaiter
方法添加到Condition
队列中:
private Node addConditionWaiter() {
// 判断当前线程是否为持有锁的线程
if (!isHeldExclusively()) {
throw new IllegalMonitorStateException();
}
// 获取Condition队列的尾节点
Node t = lastWaiter;
// 断开不再位于Condition队列的节点
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
// 创建Node.CONDITION模式的Node节点
Node node = new Node(Node.CONDITION);
if (t == null) {
// 队列为空的场景,将node设置为头节点
firstWaiter = node;
} else {
// 队列不为空的场景,将node添加到尾节点的后继节点上
t.nextWaiter = node;
}
// 更新尾节点
lastWaiter = node;
return node;
}
可以看到,Condition
的队列是一个朴实无华的双向链表,每次调用addConditionWaiter
方法,都会加入到Condition
队列的尾部。
注释2的部分,释放线程持有的锁,同时移出AQS的队列,内部调用了AQS的release
方法:
=final int fullyRelease(Node node) {
try {
int savedState = getState();
if (release(savedState)) {
return savedState;
}
throw new IllegalMonitorStateException();
} catch (Throwable t) {
node.waitStatus = Node.CANCELLED;
throw t;
}
}
因为已经分析过AQS的release
方法和ReentrantLock
实现的tryRelease
方法,这里我们就不过多赘述了。
注释3的部分,isOnSyncQueue
判断当前线程是否在AQS的等待队列中,我们来看此时存在的情况:
- 如果
isOnSyncQueue
返回false
,即线程不在AQS的队列中,进入自旋,调用LockSupport#park
暂停线程; - 如果
isOnSyncQueue
返回true
,即线程在AQS的队列中,不进入自旋,执行后续逻辑。
结合注释1和注释2的部分,Condition#await
的实现原理了就很清晰了:
Condition
与AQS分别维护了一个等待队列,而且是互斥的,即同一个节点只会出现在一个队列中;- 当调用
Condition#await
时,将线程添加到Condition
的队列中(注释1),同时从AQS队列中移出(注释2); - 接着判断线程位于的队列:
- 位于
Condition
队列中,该线程需要被暂停,调用LockSupport#park
; - 位于AQS队列中,该线程正在等待获取锁。
- 位于
基于以上的结论,我们已经能够猜到唤醒方法Condition#signalAll
的原理了:
- 将线程从
Condition
队列中移出,并添加到AQS的队列中; - 调用
LockSupport.unpark
唤醒线程。
至于这个猜想是否正确,我们接着来看唤醒方法的实现。
Tips:如果忘记了AQS中相关方法是如何实现的,可以回顾下《AQS的今生,构建出JUC的基础》。
signal和signalAll方法的实现
来看signal
和signalAll
的源码:
// 唤醒一个处于等待中的线程
public final void signal() {
if (!isHeldExclusively()) {
throw new IllegalMonitorStateException();
}
// 获取Condition队列中的第一个节点
Node first = firstWaiter;
if (first != null) {
// 唤醒第一个节点
doSignal(first);
}
}
// 唤醒全部处于等待中的线程
public final void signalAll() {
if (!isHeldExclusively()){
throw new IllegalMonitorStateException();
}
Node first = firstWaiter;
if (first != null) {
// 唤醒所有节点
doSignalAll(first);
}
}
两个方法唯一的差别在于头节点不为空的场景下,是调用doSignal
唤醒一个线程还是调用doSignalAll
唤醒所有线程:
private void doSignal(Node first) {
do {
// 更新头节点
if ( (firstWaiter = first.nextWaiter) == null) {
// 无后继节点的场景
lastWaiter = null;
}
// 断开节点的连接
first.nextWaiter = null;
// 唤醒头节点
} while (!transferForSignal(first) && (first = firstWaiter) != null);
}
private void doSignalAll(Node first) {
// 将Condition的队列置为空
lastWaiter = firstWaiter = null;
do {
// 断开链接
Node next = first.nextWaiter;
first.nextWaiter = null;
// 唤醒当前头节点
transferForSignal(first);
// 更新头节点
first = next;
} while (first != null);
}
可以看到,无论是doSignal
还是doSignalAll
都只是将节点移出Condition
队列,而真正起到唤醒作用的是transferForSignal
方法,从方法名可以看到该方法是通过“转移”进行唤醒的,我们来看源码:
final boolean transferForSignal(Node node) {
// 通过CAS替换node的状态
// 如果替换失败,说明node不处于Node.CONDITION状态,不需要唤醒
if (!node.compareAndSetWaitStatus(Node.CONDITION, 0)) {
return false;
}
// 将节点添加到AQS的队列的队尾
// 并返回老队尾节点,即node的前驱节点
Node p = enq(node);
int ws = p.waitStatus;
// 对前驱节点状态的判断
if (ws > 0 || !p.compareAndSetWaitStatus(ws, Node.SIGNAL)) {
LockSupport.unpark(node.thread);
}
return true;
}
transferForSignal
方法中,调用enq
方法将node
重新添加到AQS的队列中,并返回node
的前驱节点,随后对前驱节点的状态进行判断:
- 当\(ws > 0\)时,前驱节点处于
Node.CANCELLED
状态,前驱节点退出锁的争抢,node
可以直接被唤醒; - 当\(ws \leq 0\)时,通过CAS修改前驱节点的状态为
Node.SIGNAL
,设置失败时,直接唤醒node
。
《AQS的今生,构建出JUC的基础》中介绍了waitStatus
的5种状态,其中Node.SIGNAL
状态表示需要唤醒后继节点。另外,在分析shouldParkAfterFailedAcquire
方法的源码时,我们知道在进入AQS的等待队列时,需要将前驱节点的状态更新为Node.SIGNAL
。
最后来看enq
的实现:
private Node enq(Node node) {
for (;;) {
// 获取尾节点
Node oldTail = tail;
if (oldTail != null) {
// 更新当前节点的前驱节点
node.setPrevRelaxed(oldTail);
// 更新尾节点
if (compareAndSetTail(oldTail, node)) {
oldTail.next = node;
// 返回当前节点的前驱节点(即老尾节点)
return oldTail;
}
} else {
initializeSyncQueue();
}
}
}
enq
的实现就非常简单了,通过CAS更新AQS的队列尾节点,相当于添加到AQS的队列中,并返回尾节点的前驱节点。好了,唤醒方法的源码到这里就结束了,是不是和我们当初的猜想一模一样呢?
图解ConditionObject原理
功能上,Condition
实现了AQS版Object#wait
和Object#notify
,用法上也与之相似,需要先获取锁,即需要在lock
与unlock
之间调用。原理上,简单来说就是线程在AQS的队列和Condition
的队列之间的转移。
线程t持有锁
假设有线程t已经获取了ReentrantLock
,线程t1,t2和t3正在AQS的队列中等待,我们可以得到这样的结构:
线程t执行Condition#await
如果线程t中调用了Condition#await
方法,线程t进入Condition
的等待队列中,线程t1获取ReentrantLock
,并从AQS的队列中移出,结构如下:
线程t1执行Condition#await
如果线程t1中也执行了Condition#await
方法,同样线程t1进入Condition
队列中,线程t2获取到ReentrantLock
,结构如下:
线程t2执行Condition#signal
如果线程t2执行了Condition#signal
,唤醒Condition
队列中的第一个线程,此时结构如下:
通过上面的流程,我们就可以得到线程是如何在Condition
队列与AQS队列中转移的:
结语
关于Condition
的内容到这里就结束了,无论是理解,使用还是剖析原理,Condition
的难度并不高,只不过大家可能平时用得比较少,因此多少有些陌生。
最后,截止到文章发布,我应该是把开头两道题目的题解写完了吧~~
好了,今天就到这里了,Bye~~
17.AQS中的Condition是什么?的更多相关文章
- 详解AQS中的condition源码原理
摘要:condition用于显式的等待通知,等待过程可以挂起并释放锁,唤醒后重新拿到锁. 本文分享自华为云社区<AQS中的condition源码原理详细分析>,作者:breakDawn. ...
- java并发AQS中应用:以acquire()方法为例来分析线程间的同步与协作
谈到java中的并发,我们就避不开线程之间的同步和协作问题,谈到线程同步和协作我们就不能不谈谈jdk中提供的AbstractQueuedSynchronizer(翻译过来就是抽象的队列同步器)机制: ...
- 浅谈Java中的Condition条件队列,手摸手带你实现一个阻塞队列!
条件队列是什么?可能很多人和我一样答不出来,不过今天终于搞清楚了! 什么是条件队列 条件队列:当某个线程调用了wait方法,或者通过Condition对象调用了await相关方法,线程就会进入阻塞状态 ...
- Java并发指南8:AQS中的公平锁与非公平锁,Condtion
一行一行源码分析清楚 AbstractQueuedSynchronizer (二) 转自https://www.javadoop.com/post/AbstractQueuedSynchronizer ...
- AQS源码深入分析之共享模式-你知道为什么AQS中要有PROPAGATE这个状态吗?
本文基于JDK-8u261源码分析 本篇文章为AQS系列文的第二篇,前文请看:[传送门] 第一篇:AQS源码深入分析之独占模式-ReentrantLock锁特性详解 1 Semaphore概览 共享模 ...
- 多线程并发(二):聊聊AQS中的共享锁实现原理
在上一篇文章多线程并发(一)中我们通过acquire()详细地分析了AQS中的独占锁的获取流程,提到独占锁,自然少不了共享锁,所以我们这边文章就以AQS中的acquireShared()方法为例,来分 ...
- java高并发系列 - 第13天:JUC中的Condition对象
本文目标: synchronized中实现线程等待和唤醒 Condition简介及常用方法介绍及相关示例 使用Condition实现生产者消费者 使用Condition实现同步阻塞队列 Object对 ...
- canal源码之BooleanMutex(基于AQS中共享锁实现)
在看canal源码时发现一个有趣的锁实现--BooleanMutex 这个锁在canal里面多处用到,相当于一个开关,比如系统初始化/授权控制,没权限时阻塞等待,有权限时所有线程都可以快速通过 先看它 ...
- [保姆级教程] 如何在 Linux Kernel (V5.17.7) 中添加一个系统调用(System call)
最近在学习 <linux Kernel Development>,本书用的linux kernel 是v2.6 版本的.看完"系统调用"一节后,想尝试添加一个系统调用, ...
- java中的Condition协作线程接口类
在Java的Condition接口中,存在的几个方法跟Synchronized中的wait(),waitall(),wait(time ^),这个几个方法一一对应起来,但是在Lock.newCondi ...
随机推荐
- Python笔记--练习题(都来瞧一瞧,看一看嘞)
利用Python对文件进行操作 重新写入的文件如下图所示: 统计学生成绩文件的最高分最低分和平均分 Python如何统计英文文章出现最多的单词 Python统计目录下的文件大小 Python按照文件后 ...
- Spring------bean基础配置
Bean基础配置 Bean的别名配置: 在执勤已经定义好id的基础上,如果对该名称并不是很满意,但是又不是很想要去修改许多又利用到它的地方,可以选择在ApplicationContext.xml中配置 ...
- day11-MySql存储结构
MySql存储结构 参考视频:MySql存储结构 1.表空间 不同的存储引擎在磁盘文件上的结构均不一致,这里以InnoDB为例: CREATE TABLE t(id int(11)) Engine = ...
- 全网最详细中英文ChatGPT-GPT-4示例文档-从0到1快速入门计算时间复杂度应用——官网推荐的48种最佳应用场景(附python/node.js/curl命令源代码,小白也能学)
目录 Introduce 简介 setting 设置 Prompt 提示 Sample response 回复样本 API request 接口请求 python接口请求示例 node.js接口请求示 ...
- 解决ueditor表格拖拽没反应的问题
背景 ueditor作为百度推出的富文本编辑框,以功能强大著称. 笔者最近用这个编辑框做了一个自定义打印格式的功能.允许用户在富文本编辑框中设定打印格式,再实际打印时,根据关键字替换数据库中信息,然后 ...
- 多线程基础之CAS、AQS、ABA辨析
这三个单词算是多线程面试常见的问题了,也是很多小白不太懂的问题,这里给出我的理解来. 一.CAS J.U.C 并发包中的很多类都涉及到了 CAS,可以说没有 CAS 和 volatile 就没有 J. ...
- 创建镜像发布到镜像仓库【不依赖docker环境】
image 工具背景 如今,docker镜像常用于工具的分发,demo的演示,第一步就是得创建docker镜像.一般入门都会安装docker,然后用dockerFile来创建镜像,除此以外你还想过有更 ...
- TS 基础及在 Vue 中的实践:TypeScript 都发布 5.0 版本啦,现在不学更待何时!
大家好,我是 Kagol,OpenTiny 开源社区运营,TinyVue 跨端.跨框架组件库核心贡献者,专注于前端组件库建设和开源社区运营. 微软于3月16日发布了 TypeScript 5.0 版本 ...
- centos7搭建bsc全节点
Centos7搭建bsc全链节点 服务器配置 CPU:8 Cores - 16 Threads RAM:131072 MB Storage:2x 2000GB NVMe Bandwidth:8400 ...
- 详解事务模式和Lua脚本,带你吃透Redis 事务
摘要:Redis事务包含两种模式:事务模式和Lua脚本. 本文分享自华为云社区<一文讲透 Redis 事务>,作者: 勇哥java实战分享. 准确的讲,Redis事务包含两种模式:事务模式 ...