欢迎关注:王有志

期待你加入Java人的提桶跑路群:共同富裕的Java人

今天来和大家聊聊ConditionCondition为AQS“家族”提供了等待与唤醒的能力,使AQS"家族"具备了像synchronized一样暂停与唤醒线程的能力。我们先来看两道关于Condition的面试题目:

  • ConditionObject的等待与唤醒有什么区别?
  • 什么是Condition队列?

接下来,我们就按照“是什么”,“怎么用”和“如何实现”的顺序来揭开Condition的面纱吧。

Condition是什么?

Condition是Java中的接口,提供了与Object#waitObject#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();

唤醒功能上,ConditionObject的差异并不大:

  • Condition#signal\(\approx\)Object#notify

  • Condition#signalAll\(=\)Object#notifyAll

多个线程处于等待状态时,Object#notify()是“随机”唤醒线程,而Condition#signal则由具体实现决定如何唤醒线程,如:ConditionObject唤醒的是最早进入等待的线程但两个方法均只唤醒一个线程。

等待功能上,ConditionObject的共同点是:都会释放持有的资源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怎么用?

既然ConditionObject提供的等待与唤醒功能相同,那么它们的用法是不是也很相似呢?

与调用Object#waitObject#notifyAll必须处于synchronized修饰的代码中一样(获取Monitor),调用Condition#awaitCondition#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方法的实现

来看signalsignalAll的源码:

// 唤醒一个处于等待中的线程
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#waitObject#notify,用法上也与之相似,需要先获取锁,即需要在lockunlock之间调用。原理上,简单来说就是线程在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是什么?的更多相关文章

  1. 详解AQS中的condition源码原理

    摘要:condition用于显式的等待通知,等待过程可以挂起并释放锁,唤醒后重新拿到锁. 本文分享自华为云社区<AQS中的condition源码原理详细分析>,作者:breakDawn. ...

  2. java并发AQS中应用:以acquire()方法为例来分析线程间的同步与协作

    谈到java中的并发,我们就避不开线程之间的同步和协作问题,谈到线程同步和协作我们就不能不谈谈jdk中提供的AbstractQueuedSynchronizer(翻译过来就是抽象的队列同步器)机制: ...

  3. 浅谈Java中的Condition条件队列,手摸手带你实现一个阻塞队列!

    条件队列是什么?可能很多人和我一样答不出来,不过今天终于搞清楚了! 什么是条件队列 条件队列:当某个线程调用了wait方法,或者通过Condition对象调用了await相关方法,线程就会进入阻塞状态 ...

  4. Java并发指南8:AQS中的公平锁与非公平锁,Condtion

    一行一行源码分析清楚 AbstractQueuedSynchronizer (二) 转自https://www.javadoop.com/post/AbstractQueuedSynchronizer ...

  5. AQS源码深入分析之共享模式-你知道为什么AQS中要有PROPAGATE这个状态吗?

    本文基于JDK-8u261源码分析 本篇文章为AQS系列文的第二篇,前文请看:[传送门] 第一篇:AQS源码深入分析之独占模式-ReentrantLock锁特性详解 1 Semaphore概览 共享模 ...

  6. 多线程并发(二):聊聊AQS中的共享锁实现原理

    在上一篇文章多线程并发(一)中我们通过acquire()详细地分析了AQS中的独占锁的获取流程,提到独占锁,自然少不了共享锁,所以我们这边文章就以AQS中的acquireShared()方法为例,来分 ...

  7. java高并发系列 - 第13天:JUC中的Condition对象

    本文目标: synchronized中实现线程等待和唤醒 Condition简介及常用方法介绍及相关示例 使用Condition实现生产者消费者 使用Condition实现同步阻塞队列 Object对 ...

  8. canal源码之BooleanMutex(基于AQS中共享锁实现)

    在看canal源码时发现一个有趣的锁实现--BooleanMutex 这个锁在canal里面多处用到,相当于一个开关,比如系统初始化/授权控制,没权限时阻塞等待,有权限时所有线程都可以快速通过 先看它 ...

  9. [保姆级教程] 如何在 Linux Kernel (V5.17.7) 中添加一个系统调用(System call)

    最近在学习 <linux Kernel Development>,本书用的linux kernel 是v2.6 版本的.看完"系统调用"一节后,想尝试添加一个系统调用, ...

  10. java中的Condition协作线程接口类

    在Java的Condition接口中,存在的几个方法跟Synchronized中的wait(),waitall(),wait(time ^),这个几个方法一一对应起来,但是在Lock.newCondi ...

随机推荐

  1. 如何通过C#/VB.NET从PowerPoint文档中提取图片

    PowerPoint是用于制作幻灯片(演示文稿)的应用软件,每张幻灯片中都可以包含文字.图形.图形.表格.声音和影像等多种信息.有时候我们发现在PPT里面有一些精美的图片,或者其他原因想要把PPT里面 ...

  2. 深入理解 Python 虚拟机:复数(complex)的实现原理及源码剖析

    深入理解 Python 虚拟机:复数(complex)的实现原理及源码剖析 在本篇文章当中主要给大家介绍在 cpython 虚拟机当中是如何实现 复数 complex 这个数据类型的,这个数据类型在 ...

  3. C++/Qt网络通讯模块设计与实现(四)

    在C++/Qt网络通讯模块设计与实现(三)中提到了一个概念,即接受者所依附的线程:关注我微信公众号的技术朋友留言对该概念还是不解,这节就单独讲述这个概念的理论与实际应用,这种应用无处不在,因为与Qt的 ...

  4. MyBatis中的#和$有什么区别

    什么是MyBatis MyBatis是一款优秀的持久层框架,特别是在国内(国外据说还是 Hibernate 的天下)非常的流行,我们常说的SSM组合中的M指的就是#mybatis#. MyBatis支 ...

  5. SpringBoot——自定义start

    更多内容,前往 IT-BLOG 一.Mybatis 实现 start 的原理 首先在写一个自定义的 start 之前,我们先参考下 Mybatis 是如何整合 SpringBoot:mybatis-s ...

  6. Python爬取爬取明星关系并写入csv文件

    今天用Python爬取了明星关系,数据不多,一共1386条数据,代码如下: import requests from bs4 import BeautifulSoup import bs4 impor ...

  7. 使用vSphere Update Manager 升级 ESXi 主机

    使用vSphere Update Manager 升级 ESXi 主机 vSphere Update Manager  vSphere Update Manager 是用于升级.迁移.更新和修补群集主 ...

  8. 强大的 apt-get 命令

    强大的 apt-get 命令(小结) 一.ubuntu下管理软件最方便的非 apt-get 工具莫属了,它的常见用法稍微整理一下供以后参考(详细见 man apt-get ): 1.更新源,升级软件和 ...

  9. 使用 Istioctl 安装 istio

    使用 Istioctl 安装 istio 下载 Istio 转到 Istio 发布 页面,下载针对你操作系统的安装文件, 或用自动化工具下载并提取最新版本(Linux 或 macOS): [root@ ...

  10. [Excel/Word]常用函数与技巧

    1 Excel case1 同时多列筛选 同时筛选多列: 选中首行(属性行)>筛选>(筛选目标的N列) case2 IF/OR/AND/COUNTIF语句 =IF(condition,co ...