1. 背景

在之前的AbstractQueuedSynchronizer源码解读中,介绍了AQS的基本概念、互斥锁、共享锁、AQS对同步队列状态流转管理、线程阻塞与唤醒等内容。其中并不涉及Condition相关的内容。本文主要介绍AQS中Condition的实现即ConditionObject类的源码。

Condition在JUC中使用很多,最常见的就是各种BlockingQueue了。

2. Condition是什么

java.util.concurrent.locks.Condition是JUC提供的与Java的Object中wait/notify/notifyAll类似功能的一个接口,通过此接口,线程可以在某个特定的条件下等待/唤醒。

与wait/notify/notifyAll操作需要获得对象监视器类似,一个Condition实例与某个互斥锁绑定,在此Condition实例进行等待/唤醒操作的调用也需要获得互斥锁,线程被唤醒后需要再次获取到锁,否则将继续等待。

而与原生的wait/notify/notifyAll等API不同的地方在于,JUC提供的Condition具有更丰富的功能,例如等待可以响应/不响应中断,可以设定超时时间或是等待到某个具体时间点。

此外一把互斥锁可以绑定多个Condition,这意味着在同一把互斥锁上竞争的线程可以在不同的条件下等待,唤醒时可以根据条件来唤醒线程,这是Object中的wait/notify/notifyAll不具备的机制

3. 代码解读

3.1 套路

JUC中Condition接口的主要实现类是AQS的内部类ConditionObject,它内部维护了一个队列,我们可以称之为条件队列,在某个Condition上等待的线程被signal/signalAll后,ConditionObject会将对应的节点转移到外部类AQS的等待队列中,线程需要获取到AQS等待队列的锁,才可以继续恢复执行后续的用户代码。

这里给出一个流程:

await流程:
1. 创建节点加入到条件队列
2. 释放互斥锁
3. 只要没有转移到同步队列就阻塞(等待其他线程调用signal/signalAll或是被中断)
4. 重新获取互斥锁
signal流程:
1. 将队列中第一个节点转移到同步队列
2. 根据情况决定是否要唤醒对应线程



这里以我之前在[AbstractQueuedSynchronizer源码解读]画的AQS状态流转图来说明下:

如果一个节点通过ConditionObject#await等方法调用初始化后,在被唤醒之后,会将状态切换至0,也即无状态,随后进入AQS的同步队列,此后就与一般的争锁无异了。

3.2 await方法

public final void await() throws InterruptedException {
// 对中断敏感。
if (Thread.interrupted())
throw new InterruptedException();
// 加到条件队列中。
Node node = addConditionWaiter();
// 完全释放互斥锁(无论锁是否可以重入),如果没有持锁,会抛出异常。
int savedState = fullyRelease(node);
int interruptMode = 0;
/*
* 只要仍未转移到同步队列就阻塞。
* 转移的情况如下:
* 1. 其它线程调用signal将当前线程节点转移到同步队列并唤醒当前线程。
* 2. 其它线程调用signalAll。
* 3. 其它线程中断了当前线程,当前线程会自行尝试进入同步队列。
*/
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
/*
* 获取中断模式。
* 在线程从park中被唤醒的时候,需要判断是否此时被中断,若中断则尝试转移到同步队列。
* 1. 中断且自行进入同步队列,返回THROW_IE(值-1),后续需要抛出InterruptedException。
* 2. 中断且未能自行进入同步队列,则说明有线程调用signal/signalAll唤醒线程并尝试转移到同步队列,
* 返回REINTERRUPT,后续重新中断线程。
* 3. 线程未被中断,返回0,此时需要重试循环判断是否上了同步队列。
*/
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 重新获取互斥锁过程中如果中断并且interruptMode不为"抛出异常",设置为REINTERRUPT。
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
// 如果线程发生过中断则根据THROW_IE或是REINTERRUPT分别抛出异常或者重新中断。
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
} private Node addConditionWaiter() {
Node t = lastWaiter;
/*
* 如果条件队列中最后一个waiter节点状态为取消,
* 则调用unlinkCancelledWaiters清理队列。
*/
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
// 重读lastWaiter。
t = lastWaiter;
}
Node node = new Node(Thread.currentThread(), Node.CONDITION);
// t如果为null, 初始化firstWaiter为当前节点。
if (t == null)
firstWaiter = node;
else
// 将队尾的next连接到node。
t.nextWaiter = node;
lastWaiter = node;
return node;
} /**
* 移除队列中所有取消节点。
*/
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)
// 如果trail为null,取当前节点的后继作为头节点的值(next可以为null)。
firstWaiter = next;
else
// 否则把trail连接到当前节点的后继。
trail.nextWaiter = next;
// 如果当前节点没有后继了, 更新lastWaiter为trail, 即上一个非取消节点。
if (next == null)
lastWaiter = trail;
}
else
trail = t;
t = next;
}
} final boolean isOnSyncQueue(Node node) {
/*
* 节点状态为CONDITION一定是在条件队列,
* 或者如果prev为null也一定是在条件队列。
*
* 同步队列里的节点prev为null只可能是获取到锁后调用setHead清为null,
* 新入队的节点prev值是不会为null的。
* 另外,条件队列里节点是用nextWaiter来维护的,不用next和prev。
*/
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
/*
* 如果next不为null,一定是在同步队列的。
* 这里值得一提的是在AQS的cancelAcquire方法中,
* 一个节点将自己移除出队列的时候会把自己的next域指向自己。
* 即node.next = node;
*
* 从GC效果上来看node.next = node和node.next = null无异,
* 但是这对此处next不为null一定在同步队列上来说,
* 这样可以将节点在同步队列上被取消的情况与普通情况归一化判断。
*/
if (node.next != null)
return true;
/*
* 有可能node.prev的值不为null,但还没在队列中,因为入队时CAS队列的tail可能失败。
* 这是从tail向前遍历一次,确定是否已经在同步队列上。
*/
return findNodeFromTail(node);
} /**
* 从队列尾部向前遍历判断节点是否在队列中。
*/
private boolean findNodeFromTail(Node node) {
Node t = tail;
for (;;) {
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
} private int checkInterruptWhileWaiting(Node node) {
/*
* 1. 线程未中断返回0
* 2. 线程中断且入同步队列成功,返回THROW_IE表示后续要抛出InterruptedException。
* 3. 线程中断且未能入同步队列(由于被signal方法唤醒),则返回REINTERRUPT表示后续重新中断。
*/
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
} final boolean transferAfterCancelledWait(Node node) {
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
enq(node);
return true;
}
/*
* 上面CAS失败的原因是signal()方法被调用,状态已经被抢先更新了。
* 这时需要自旋等待节点成功进入同步队列,否则会影响后续的重新获取锁acquireQueued()方法。
* 因为acquireQueued必须在节点成功入队后才可以调用。
*/
while (!isOnSyncQueue(node))
Thread.yield();
return false;
} /**
* THROW_IE则抛出InterruptedException,
* REINTERRUPT则重新中断当前线程。
*/
private void reportInterruptAfterWait(int interruptMode)
throws InterruptedException {
if (interruptMode == THROW_IE)
throw new InterruptedException();
else if (interruptMode == REINTERRUPT)
selfInterrupt();
} static void selfInterrupt() {
Thread.currentThread().interrupt();
}

3.3 signal/signalAll方法

public final void signal() {
// 检查互斥锁持有情况。
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
} private void doSignal(Node first) {
do {
// 将firstWaiter设置为后继节点,如果为null,则置lastWaiter为null。
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
// 断开连接。
first.nextWaiter = null;
/*
* 如果转移失败并且下一个节点不为null,则重试。
* 在这里转移失败只可能因为节点被取消。
*/
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
} public final void signalAll() {
// 检查互斥锁持有情况。
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignalAll(first);
} private void doSignalAll(Node first) {
// 将firstWaiter和lastWaiter先清为null。
lastWaiter = firstWaiter = null;
// 从first开始一直遍历到第一个null节点。
do {
Node next = first.nextWaiter;
first.nextWaiter = null;
transferForSignal(first);
first = next;
} while (first != null);
} final boolean transferForSignal(Node node) {
// 必须将状态从CONDITION流转到0,如果失败则说明节点被取消,因为这里不会存在signal的竞争。
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false; // 插入同步队列。
Node p = enq(node);
int ws = p.waitStatus;
/*
* 如果前驱节点状态为取消或者无法将状态CAS到SIGNAL(比如可能前驱在此期间被取消了),
* 则需要唤醒参数node节点对应的线程,使其能开始尝试争锁。
*
* 如果将前驱状态切到SIGNAL了,则由相应线程在释放锁之后唤醒node节点对应线程。
*/
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}

4. 思考与总结

至此,已经对ConditionObject的await/signal/signalAll方法源码进行了分析。对于中断不敏感的awaitUninterruptibly, 带有时限的awaitNanos由于大致套路与await无异,不作冗述。

ConditionObject的firstWaiter/lastWaiter以及AQS.Node的nextWaiter都是没有volatile修饰的。这是因为ConditionObject假设在await/signal/signalAll等方法的调用是已经持有互斥锁的。

个人认为ConditionObject这样的设计是有个问题的。即按照Condition接口的定义,在不持锁情况下调用await由子类决定如何处理,通常是抛出InterruptedException。但如果同时有持锁和不持锁的线程调用await方法,可能会对ConditionObject的内部队列造成破坏,后果就是有些成功调用await方法的线程可能永远没有办法被唤醒,因为无法通过队列追溯到它们。也就是非法调用会抛出异常,但仍然会对内部数据结构造成破坏,这其实是有一些不合理的,至少是可以改进的地方。

最简单的处理方式是,对于不持锁的请求抛出异常,不应该依靠await -> fullyRelease这一步来抛出异常,此时按照流程已经调用过addConditionWaiter了。可以在await这类方法前面与signal/signalAll一样预检查一次持锁情况:

if (!isHeldExclusively())
throw new IllegalMonitorStateException();

整体而言,ConditionObject中代码对GC友好,逻辑缜密,读过之后受益匪浅。

AbstractQueuedSynchronizer源码解读--续篇之Condition的更多相关文章

  1. AbstractQueuedSynchronizer源码解读

    1. 背景 AQS(java.util.concurrent.locks.AbstractQueuedSynchronizer)是Doug Lea大师创作的用来构建锁或者其他同步组件(信号量.事件等) ...

  2. AbstractQueuedSynchronizer 源码解读(转载)

    转载文章,拜读了一下原文感觉很不错,转载一下,侵删 链接地址:http://objcoding.com/2019/05/05/aqs-exclusive-lock/ Java并发之AQS源码分析(一) ...

  3. AbstractQueuedSynchronizer源码分析

    AbstractQueuedSynchronizer源码分析 前提 AQS(java.util.concurrent.locks.AbstractQueuedSynchronizer)是并发编程大师D ...

  4. SDWebImage源码解读之SDWebImageDownloaderOperation

    第七篇 前言 本篇文章主要讲解下载操作的相关知识,SDWebImageDownloaderOperation的主要任务是把一张图片从服务器下载到内存中.下载数据并不难,如何对下载这一系列的任务进行设计 ...

  5. AFNetworking 3.0 源码解读(三)之 AFURLRequestSerialization

    这篇就讲到了跟请求相关的类了 关于AFNetworking 3.0 源码解读 的文章篇幅都会很长,因为不仅仅要把代码进行详细的的解释,还会大概讲解和代码相关的知识点. 上半篇: URI编码的知识 关于 ...

  6. SDWebImage源码解读之SDWebImageManager

    第九篇 前言 SDWebImageManager是SDWebImage中最核心的类了,但是源代码确是非常简单的.之所以能做到这一点,都归功于功能的良好分类. 有了SDWebImageManager这个 ...

  7. Java并发系列[4]----AbstractQueuedSynchronizer源码分析之条件队列

    通过前面三篇的分析,我们深入了解了AbstractQueuedSynchronizer的内部结构和一些设计理念,知道了AbstractQueuedSynchronizer内部维护了一个同步状态和两个排 ...

  8. CyclicBarrier源码解读

    1. 简介 JUC中的CyclicBarrier提供了一种多线程间的同步机制,可以让多个线程在barrier等待其它线程到达barrier.正如其名CyclicBarrier含义就是可以循环使用的屏障 ...

  9. ScheduledThreadPoolExecutor源码解读

    1. 背景 在之前的博文--ThreadPoolExecutor源码解读已经对ThreadPoolExecutor的实现原理与源码进行了分析.ScheduledExecutorService也是我们在 ...

随机推荐

  1. 最新IP数据库 存储优化 查询性能优化 每秒解析上千万

    高性能IP数据库格式详解 每秒解析1000多万ip  qqzeng-ip-ultimate.dat 3.0版 编码:UTF8     字节序:Little-Endian 返回规范字段(如:亚洲|中国| ...

  2. selenium用法详解

    selenium用法详解 selenium主要是用来做自动化测试,支持多种浏览器,爬虫中主要用来解决JavaScript渲染问题. 模拟浏览器进行网页加载,当requests,urllib无法正常获取 ...

  3. url,href,src之间的区别

    发现自己居然没把url.href.src关系及使用搞清楚,今天就理一下.主要包括:url.src.href定义以及使用区别. URL(Uniform Resource Locator) 统一资源定位符 ...

  4. AWS stolen CPU

    故事的开头是这样的: 一天我正在吃饭,突然就收到了服务器告警(cpu high load),吓的我饭也没吃好,只吃了三碗就回去处理故障了,我在监控上看到了这样子的图: 看见了吧,吃饭那段时间cpu一下 ...

  5. 简述SharePoint designer 工作流实现用户撤回申请得解决方案,how to revoke/recall application in SharePoint designer workflow

    在工作中很容易碰到用户提出,需要可以撤回申请得需求.有这么几种实现方法. 方案一:designer中加一个parallel(平行)得anction, 然后在里面添加两个平行运行得审批 第一个是给真正得 ...

  6. Drainage Ditches~网络流模板

    Description Every time it rains on Farmer John's fields, a pond forms over Bessie's favorite clover ...

  7. Mybatis-简单基于源码了解获取动态代理对象

    这是我们要测试的代码 OderDao就是我们要需要获取的对象. 首先我们根据传入的参数,进入SqlSessionFactoryBuilder 中的对应的build 方法,第一步创键XMLConfigB ...

  8. 在容器中运行 Jenkins pipeline 任务

    持续集成中的 pipeline 技术和 docker 都是当前正在发展的主流方向,当然把它们结合起来在 CI/CD 过程中发挥出更强大的威力也是大家共同的目标.本文将介绍如何在 Jenkins pip ...

  9. codesforces 671D Roads in Yusland

    Mayor of Yusland just won the lottery and decided to spent money on something good for town. For exa ...

  10. ●UVa 11346 Probability

    题链: https://vjudge.net/problem/UVA-11346题解: 连续概率,积分 由于对称性,我们只用考虑第一象限即可. 如果要使得面积大于S,即xy>S, 那么可以选取的 ...