文章部分代码和照片来自参考资料

问题 :

  • ConditionObject  的 await 和 signal 方法是如何实现的

ConditonObject

ConditionObject 继承 Condition 这个接口, 看一下这个接口的注解说明 :

Condition factors out the Object monitor methods (wait, notify and notifyAll) into distinct objects to give the effect of having multiple wait-sets per object, by combining them with the use of arbitrary Lock implementations. Where a Lock replaces the use of synchronized methods and statements, a Condition replaces the use of the Object monitor methods.

如果Lock替换了synchronized方法和语句的使用,则Condition将替换Object监视方法的使用。

Condition 经常可以用在生产者-消费者的场景中,ArrayBlockingQueue 采用这种方式实现了生产者-消费者.

ConditionObject 是 AQS里的一个对象,继承Condition 接口,上一节我们提到AQS 通过同步队列(sync queue )和 等待队列(wait queue )还有 状态变量(statue)进行并发控制。这节我们要讲的就是在等待队列的操作。

下面是 wait queue 和 sync queue 的图例。

await 和 signal 方法

      代码分析来自于 一行一行源码分析清楚 AbstractQueuedSynchronizer (二)

这两个方法可以用下面两种两张图来描述。其中await 是释放所有的锁,然后将节点加入到 wait queue ,然后等待唤醒。 signal 方法是从wait queue 移动到 sync queue 中,然后唤醒。

图一. await 方法

图二. signal 方法

Conditon 的方法实现 基于 ReetranLock 。下面源码分析会涉及到。

Condition 的await 方法 包括的操作有 :

  • If current thread is interrupted, throw InterruptedException.
  • Save lock state returned by getState.
  • Invoke release with saved state as argument, throwing IllegalMonitorStateException if it fails.
  • Block until signalled or interrupted.
  • Reacquire by invoking specialized version of acquire with saved state as argument.
    If interrupted while blocked in step 4, throw InterruptedException.
  1 // 首先,这个方法是可被中断的,不可被中断的是另一个方法 awaitUninterruptibly()
2 // 这个方法会阻塞,直到调用 signal 方法(指 signal() 和 signalAll(),下同),或被中断
3 public final void await() throws InterruptedException {
4 if (Thread.interrupted())
5 throw new InterruptedException();
6 // 添加到 condition 的条件队列中
7 Node node = addConditionWaiter();
8 // 释放锁,返回值是释放锁之前的 state 值
9 int savedState = fullyRelease(node);
10 int interruptMode = 0;
11 // 这里退出循环有两种情况,之后再仔细分析
12 // 1. isOnSyncQueue(node) 返回 true,即当前 node 已经转移到阻塞队列了
13 // 2. checkInterruptWhileWaiting(node) != 0 会到 break,然后退出循环,代表的是线程中断
14 while (!isOnSyncQueue(node)) {
15 LockSupport.park(this);
16 if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
17 break;
18 }
19 // 被唤醒后,抢锁,抢不到将进入阻塞队列,等待获取锁
20 if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
21 interruptMode = REINTERRUPT;
22 if (node.nextWaiter != null) // clean up if cancelled
23 unlinkCancelledWaiters();
24 if (interruptMode != 0)
25 reportInterruptAfterWait(interruptMode);
26 }
  1 // 将当前线程对应的节点入队,插入队尾
2 private Node addConditionWaiter() {
3 Node t = lastWaiter;
4 // 如果条件队列的最后一个节点取消了,将其清除出去
5 if (t != null && t.waitStatus != Node.CONDITION) {
6 // 这个方法会遍历整个条件队列,然后会将已取消的所有节点清除出队列
7 unlinkCancelledWaiters();
8 t = lastWaiter;
9 }
10 Node node = new Node(Thread.currentThread(), Node.CONDITION);
11 // 如果队列为空
12 if (t == null)
13 firstWaiter = node;
14 else
15 t.nextWaiter = node;
16 lastWaiter = node;
17 return node;
18 }
19 在addWaiter 方法中,有一个 unlinkCancelledWaiters() 方法,该方法用于清除队列中已经取消等待的节点。
20
21 当 await 的时候如果发生了取消操作(这点之后会说),或者是在节点入队的时候,发现最后一个节点是被取消的,会调用一次这个方法。
22
23 // 等待队列是一个单向链表,遍历链表将已经取消等待的节点清除出去
24 // 纯属链表操作,很好理解,看不懂多看几遍就可以了
25 private void unlinkCancelledWaiters() {
26 Node t = firstWaiter;
27 Node trail = null;
28 while (t != null) {
29 Node next = t.nextWaiter;
30 // 如果节点的状态不是 Node.CONDITION 的话,这个节点就是被取消的
31 if (t.waitStatus != Node.CONDITION) {
32 t.nextWaiter = null;
33 if (trail == null)
34 firstWaiter = next;
35 else
36 trail.nextWaiter = next;
37 if (next == null)
38 lastWaiter = trail;
39 }
40 else
41 trail = t;
42 t = next;
43 }
44 }

ReentranLock 是可重入的,所以释放所有的锁。

  1 // 首先,我们要先观察到返回值 savedState 代表 release 之前的 state 值
2 // 对于最简单的操作:先 lock.lock(),然后 condition1.await()。
3 // 那么 state 经过这个方法由 1 变为 0,锁释放,此方法返回 1
4 // 相应的,如果 lock 重入了 n 次,savedState == n
5 // 如果这个方法失败,会将节点设置为"取消"状态,并抛出异常 IllegalMonitorStateException
6 final int fullyRelease(Node node) {
7 boolean failed = true;
8 try {
9 int savedState = getState();
10 // 这里使用了当前的 state 作为 release 的参数,也就是完全释放掉锁,将 state 置为 0
11 if (release(savedState)) {
12 failed = false;
13 return savedState;
14 } else {
15 throw new IllegalMonitorStateException();
16 }
17 } finally {
18 if (failed)
19 node.waitStatus = Node.CANCELLED;
20 }
21 }
  1 // 在节点入条件队列的时候,初始化时设置了 waitStatus = Node.CONDITION
2 // 前面我提到,signal 的时候需要将节点从条件队列移到阻塞队列,
3 // 这个方法就是判断 node 是否已经移动到阻塞队列了
4 final boolean isOnSyncQueue(Node node) {
5 // 移动过去的时候,node 的 waitStatus 会置为 0,这个之后在说 signal 方法的时候会说到
6 // 如果 waitStatus 还是 Node.CONDITION,也就是 -2,那肯定就是还在条件队列中
7 // 如果 node 的前驱 prev 指向还是 null,说明肯定没有在 阻塞队列
8 if (node.waitStatus == Node.CONDITION || node.prev == null)
9 return false;
10 // 如果 node 已经有后继节点 next 的时候,那肯定是在阻塞队列了
11 if (node.next != null)
12 return true;
13
14 // 这个方法从阻塞队列的队尾开始从后往前遍历找,如果找到相等的,说明在阻塞队列,否则就是不在阻塞队列
15
16 // 可以通过判断 node.prev() != null 来推断出 node 在阻塞队列吗?答案是:不能。
17 // 这个可以看上篇 AQS 的入队方法,首先设置的是 node.prev 指向 tail,
18 // 然后是 CAS 操作将自己设置为新的 tail,可是这次的 CAS 是可能失败的。
19
20 // 调用这个方法的时候,往往我们需要的就在队尾的部分,所以一般都不需要完全遍历整个队列的
21 return findNodeFromTail(node);
22 }
23
24 // 从同步队列的队尾往前遍历,如果找到,返回 true
25 private boolean findNodeFromTail(Node node) {
26 Node t = tail;
27 for (;;) {
28 if (t == node)
29 return true;
30 if (t == null)
31 return false;
32 t = t.prev;
33 }
34 }
35 }

  1 int interruptMode = 0;
2 while (!isOnSyncQueue(node)) {
3 // 线程挂起
4 LockSupport.park(this);
5
6 if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
7 break;
8 }

回到前面的循环,isOnSyncQueue(node) 返回 false 的话,那么进到 LockSupport.park(this); 这里线程挂起。

接下来就是 signal 移动元素到同步队列,释放锁的时候唤醒线程,转移到阻塞队列为了大家理解,这里我们先看唤醒操作,因为刚刚到 LockSupport.park(this); 把线程挂起了,等待唤醒。唤醒操作通常由另一个线程来操作,就像生产者-消费者模式中,如果线程因为等待消费而挂起,那么当生产者生产了一个东西后,会调用 signal 移动元素到同步队列,释放锁的时候唤醒线程来消费。

  1
2 // 唤醒等待了最久的线程
3 // 其实就是,将这个线程对应的 node 从条件队列转移到阻塞队列
4 public final void signal() {
5 // 调用 signal 方法的线程必须持有当前的独占锁
6 if (!isHeldExclusively())
7 throw new IllegalMonitorStateException();
8 Node first = firstWaiter;
9 if (first != null)
10 doSignal(first);
11 }
12
13 // 从条件队列队头往后遍历,找出第一个需要转移的 node
14 // 因为前面我们说过,有些线程会取消排队,但是还在队列中
15 private void doSignal(Node first) {
16 do {
17 // 将 firstWaiter 指向 first 节点后面的第一个
18 // 如果将队头移除后,后面没有节点在等待了,那么需要将 lastWaiter 置为 null
19 if ( (firstWaiter = first.nextWaiter) == null)
20 lastWaiter = null;
21 // 因为 first 马上要被移到阻塞队列了,和条件队列的链接关系在这里断掉
22 first.nextWaiter = null;
23 } while (!transferForSignal(first) &&
24 (first = firstWaiter) != null);
25 // 这里 while 循环,如果 first 转移不成功,那么选择 first 后面的第一个节点进行转移,依此类推
26 }
27
28 // 将节点从条件队列转移到阻塞队列
29 // true 代表成功转移
30 // false 代表在 signal 之前,节点已经取消了
31 final boolean transferForSignal(Node node) {
32
33 // CAS 如果失败,说明此 node 的 waitStatus 已不是 Node.CONDITION,说明节点已经取消,
34 // 既然已经取消,也就不需要转移了,方法返回,转移后面一个节点
35 // 否则,将 waitStatus 置为 0
36 if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
37 return false;
38
39 // enq(node): 自旋进入阻塞队列的队尾
40 // 注意,这里的返回值 p 是 node 在阻塞队列的前驱节点
41 Node p = enq(node);
42 int ws = p.waitStatus;
43 // ws > 0 说明 node 在阻塞队列中的前驱节点取消了等待锁,直接唤醒 node 对应的线程。唤醒之后会怎么样,后面再解释
44 // 如果 ws <= 0, 那么 compareAndSetWaitStatus 将会被调用,上篇介绍的时候说过,节点入队后,需要把前驱节点的状态设为 Node.SIGNAL(-1)
45 if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
46 // 如果前驱节点取消或者 CAS 失败,会进到这里唤醒线程,之后的操作看下一节
47 LockSupport.unpark(node.thread);
48 return true;
49

注意 :

  • 调用 signal 前要先获得锁
  • 正常情况下,signal 方法只是对元素进行了移动(从wait queue 到 sync queue),真正的唤醒操作在释放锁的代码里。

那么阻塞的线程现在继续执行,我们可以看到,即使被唤醒依旧要继续抢。

  1     /**
2 * Acquires in exclusive uninterruptible mode for thread already in
3 * queue. Used by condition wait methods as well as acquire.
4 *
5 * @param node the node
6 * @param arg the acquire argument
7 * @return {@code true} if interrupted while waiting
8 */
9 final boolean acquireQueued(final Node node, int arg) {
10 boolean failed = true;
11 try {
12 boolean interrupted = false;
13 for (;;) {
14 final Node p = node.predecessor();
15 if (p == head && tryAcquire(arg)) {
16 setHead(node);
17 p.next = null; // help GC
18 failed = false;
19 return interrupted;
20 }
21 if (shouldParkAfterFailedAcquire(p, node) &&
22 parkAndCheckInterrupt())
23 interrupted = true;
24 }
25 } finally {
26 if (failed)
27 cancelAcquire(node);
28 }
29 }

注意 :

  • await 方法被唤醒后依旧要抢,要是抢不到就继续阻塞。

要是CAS 成功,检查中断状态

  1 // 1. 如果在 signal 之前已经中断,返回 THROW_IE
2 // 2. 如果是 signal 之后中断,返回 REINTERRUPT
3 // 3. 没有发生中断,返回 0
4 private int checkInterruptWhileWaiting(Node node) {
5 return Thread.interrupted() ?
6 (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
7 0;
8 }
  1 // 只有线程处于中断状态,才会调用此方法
2 // 如果需要的话,将这个已经取消等待的节点转移到阻塞队列
3 // 返回 true:如果此线程在 signal 之前被取消,
4 final boolean transferAfterCancelledWait(Node node) {
5 // 用 CAS 将节点状态设置为 0
6 // 如果这步 CAS 成功,说明是 signal 方法之前发生的中断,因为如果 signal 先发生的话,signal 中会将 waitStatus 设置为 0
7 if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
8 // 将节点放入阻塞队列
9 // 这里我们看到,即使中断了,依然会转移到阻塞队列
10 enq(node);
11 return true;
12 }
13
14 // 到这里是因为 CAS 失败,肯定是因为 signal 方法已经将 waitStatus 设置为了 0
15 // signal 方法会将节点转移到阻塞队列,但是可能还没完成,这边自旋等待其完成
16 // 当然,这种事情还是比较少的吧:signal 调用之后,没完成转移之前,发生了中断
17 while (!isOnSyncQueue(node))
18 Thread.yield();
19 return false;
20 }

小提示

在AQS 的子类实现中,Conditon 常常会配合 ReentranLock  使用,例如 LinkBlockingQueue 。 要注意的是ConditionObject的 await 方法会释放掉所有的锁。show me code !

  1 public class LinkBlockingQueueTest {
2 private ReentrantLock mLock = new ReentrantLock();
3 private Condition mConditionObject = mLock.newCondition();
4
5 public void testMethod() throws InterruptedException {
6
7 final ReentrantLock methodLock = this.mLock;
8 methodLock.lockInterruptibly();
9 try {
10 mConditionObject.await();
11 System.out.println("sss");
12 } finally {
13 methodLock.unlock();
14 }
15 }
16
17 public void testMethod1() throws InterruptedException {
18
19 final ReentrantLock methodLock = this.mLock;
20 methodLock.lockInterruptibly();
21 try {
22 System.out.println("s1");
23 } finally {
24 methodLock.unlock();
25 }
26 }
27
28 }
  1   public static void main(String[] args) throws InterruptedException {
2
3 LinkBlockingQueueTest obj = new LinkBlockingQueueTest();
4
5 new Thread(() -> {
6 try {
7 obj.testMethod();
8 } catch (InterruptedException e) {
9 e.printStackTrace();
10 }
11 }).start();
12
13 Thread.sleep(1000L * 2);
14 new Thread(() -> {
15 try {
16 obj.testMethod1();
17 } catch (InterruptedException e) {
18 e.printStackTrace();
19 }
20 }).start();
21
22 }

会输出 s1 。

为什么有了sync queue 还需要 wait queue ?

ConditionObject  里的await方法会(假如这个节点已经存在sync queue)释放锁移入 wait queue , signal 方法则是重新移入到 sync queue ,那么我们就可以知道了 wait queue 的作用是临时存放节点,移除在 sync queue 的节点(假如存在),再插入到 sync queue 的队尾。它的作用我们可以在阅读ArrayBlockingQueue源码时就可以感受到了。

参考资料

java 并发(五)---AbstractQueuedSynchronizer(2)的更多相关文章

  1. java 并发(五)---AbstractQueuedSynchronizer

    文章部分图片和代码来自参考文章. LockSupport 和 CLH 和 ConditionObject 阅读源码首先看一下注解 ,知道了大概的意思后,再进行分析.注释一开始就进行了概括.AQS的实现 ...

  2. java 并发(五)---AbstractQueuedSynchronizer(4)

    问题 : rwl 的底层实现是什么,应用场景是什么 读写锁 ReentrantReadWriteLock 首先我们来了解一下 ReentrantReadWriteLock 的作用是什么?和 Reent ...

  3. java 并发(五)---AbstractQueuedSynchronizer(5)

    问题 : ArrayBlockQueue 和 LinkedBlockQueue 的区别 两者的实现又是怎么样的 应用场景 BlockingQueue 概述 blockingQueue 是个接口,从名字 ...

  4. java 并发(五)---AbstractQueuedSynchronizer(3)

           文章代码分析和部分图片来自参考文章 问题 : CountDownLatch  和 CyclicBarrier 的区别 认识 CountDownLatch 分析这个类,首先了解一下它所可以 ...

  5. Java并发基础类AbstractQueuedSynchronizer的实现原理简介

    1.引子 Lock接口的主要实现类ReentrantLock 内部主要是利用一个Sync类型的成员变量sync来委托Lock锁接口的实现,而Sync继承于AbstractQueuedSynchroni ...

  6. Java并发框架AbstractQueuedSynchronizer(AQS)

    1.前言 本文介绍一下Java并发框架AQS,这是大神Doug Lea在JDK5的时候设计的一个抽象类,主要用于并发方面,功能强大.在新增的并发包中,很多工具类都能看到这个的影子,比如:CountDo ...

  7. Java并发(五):并发,迭代器和容器

    在随后的博文中我会继续分析并发包源码,在这里,得分别谈谈容器类和迭代器及其源码,虽然很突兀,但我认为这对于学习Java并发很重要; ConcurrentModificationException: J ...

  8. Java并发编程-AbstractQueuedSynchronizer源码分析

    简介 提供了一个基于FIFO队列,可以用于构建锁或者其他相关同步装置的基础框架.该同步器(以下简称同步器)利用了一个int来表示状态,期望它能够成为实现大部分同步需求的基础.使用的方法是继承,子类通过 ...

  9. Java并发:AbstractQueuedSynchronizer(AQS)

    队列同步器 AbstractQueuedSynchronizer 是一个公共抽象类.提供一个同步器框架,用于实现依赖于先进先出(FIFO)等待队列的阻塞锁和相关同步器(信号量,事件等).使用一个 in ...

随机推荐

  1. 食物(矩阵快速幂)(DP)

    这个题..我们可以想到用递推写!!qwq(好吧,其实我的DP水平不高啊qwq) 就是我们以两个为单位(一共九种组合情况),然后往后面推下一位的情况. 通过手动模拟,我们可以找到它们之间的递推关系(详见 ...

  2. 【09】循序渐进学 docker:docker swarm

    写在前面的话 至此,docker 的基础知识已经了解的差不多了,接下来就来谈谈对于 docker 容器,我们如何来管理它. docker swarm 在学习 docker swarm 之前,得先知道容 ...

  3. vue和jQuery的区别

    从jquery到vue或者说是到mvvm的转变是一个思想的转变,是将原有的直接操作dom的思想转变到操作数据上去 vue和jquey对比 jQuery是使用选择器($)选取DOM对象,对其进行赋值.取 ...

  4. python 特性:height-->while

    """ 出题:height 女生找对象 男生在1米-1.5米之间 小强你在哪里? 男生在1.5-1.7米之间 没有安全感 男生在1.7 - 1.8米之间 帅哥 留个电话 ...

  5. “全栈2019”Java第三十九章:构造函数、构造方法、构造器

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...

  6. 免费观看vip/要劵的电影

    免费观看vip/要劵的电影 1.在爱奇艺/腾讯视频中复制电影的连接 2.复制连接到这个网站中(http://www.qmaile.com/) 3.粘贴路径到这个网站相应的位置 4.点击go ,等待解析 ...

  7. 主流服务器虚拟化技术简单使用——KVM(二)

    通过Linux工具管理KVM 主流服务器虚拟化技术简单使用——KVM(一)部署了一台KVM主机,提到KVM可以通过命令行工具(virt-install.virsh)和GUI工具(virt-manage ...

  8. 51nod1238. 最小公倍数之和 V3(数论)

    题目链接 https://www.51nod.com/Challenge/Problem.html#!#problemId=1238 题解 本来想做个杜教筛板子题结果用另一种方法过了...... 所谓 ...

  9. Flutter 实现退出登录功能,应用退出到登录界面 | 返回应用首页

    1. 使用场景:退出登录./// 路由作用:移除 [ModalRoute.withName("/loginPage")] 除外的所有界面,并跳转到 ["/loginPag ...

  10. STM32的GPIO工作原理 | 附电路图详细分析

    STM32的GPIO介绍 STM32引脚说明 GPIO是通用输入/输出端口的简称,是STM32可控制的引脚.GPIO的引脚与外部硬件设备连接,可实现与外部通讯.控制外部硬件或者采集外部硬件数据的功能. ...