相比于独占锁跟共享锁,AbstractQueuedSynchronizer中的条件队列可能被关注的并不是很多,但它在阻塞队列的实现里起着至关重要的作用,同时如果想全面了解AQS,条件队列也是必须要学习的。

原文地址:http://www.jianshu.com/p/3f8b08ca21cd

这篇文章会涉及到AQS中独占锁跟共享锁的一些知识,如果你已经对这两块内容很了解了,那就直接往下看。否则在读本文之前还是建议读者先去看看我之前写的两篇文章温习一下。

深入浅出AQS之独占锁模式

深入浅出AQS之共享锁模式

一、使用场景介绍

区别于前面两篇文章,可能之前很多人都没有太在意AQS中的这块内容,所以这篇文章我们先来看下条件队列的使用场景:

  1. //首先创建一个可重入锁,它本质是独占锁
  2. private final ReentrantLock takeLock = new ReentrantLock();
  3. //创建该锁上的条件队列
  4. private final Condition notEmpty = takeLock.newCondition();
  5. //使用过程
  6. public E take() throws InterruptedException {
  7. //首先进行加锁
  8. takeLock.lockInterruptibly();
  9. try {
  10. //如果队列是空的,则进行等待
  11. notEmpty.await();
  12. //取元素的操作...
  13. //如果有剩余,则唤醒等待元素的线程
  14. notEmpty.signal();
  15. } finally {
  16. //释放锁
  17. takeLock.unlock();
  18. }
  19. //取完元素以后唤醒等待放入元素的线程
  20. }

上面的代码片段截取自LinkedBlockingQueue,是Java常用的阻塞队列之一。

从上面的代码可以看出,条件队列是建立在锁基础上的,而且必须是独占锁(原因后面会通过源码分析)。

二、执行过程概述

等待条件的过程:

  1. 在操作条件队列之前首先需要成功获取独占锁,不然直接在获取独占锁的时候已经被挂起了。
  2. 成功获取独占锁以后,如果当前条件还不满足,则在当前锁的条件队列上挂起,与此同时释放掉当前获取的锁资源。这里可以考虑一下如果不释放锁资源会发生什么?
  3. 如果被唤醒,则检查是否可以获取独占锁,否则继续挂起。

条件满足后的唤醒过程(以唤醒一个节点为例,也可以唤醒多个):

  1. 把当前等待队列中的第一个有效节点(如果被取消就无效了)加入同步队列等待被前置节点唤醒,如果此时前置节点被取消,则直接唤醒该节点让它重新在同步队列里适当的尝试获取锁或者挂起。

注:说到这里必须要解释一个知识点,整个AQS分为两个队列,一个同步队列,一个条件队列。只有同步队列中的节点才能获取锁。前面两篇独占锁共享锁文章中提到的加入队列就是同步队列。条件队列中所谓的唤醒是把节点从条件队列移到同步队列,让节点有机会去获取锁。

二、源码深入分析

下面的代码稍微复杂一点,因为它考虑了中断的处理情况。我由于想跟文章开头的代码片段保持一致,所以选取了该方法进行说明。如果只想看核心逻辑的话,那推荐读者看看awaitUninterruptibly()方法的源码。

  1. //条件队列入口,参考上面的代码片段
  2. public final void await() throws InterruptedException {
  3. //如果当前线程被中断则直接抛出异常
  4. if (Thread.interrupted())
  5. throw new InterruptedException();
  6. //把当前节点加入条件队列
  7. Node node = addConditionWaiter();
  8. //释放掉已经获取的独占锁资源
  9. int savedState = fullyRelease(node);
  10. int interruptMode = 0;
  11. //如果不在同步队列中则不断挂起
  12. while (!isOnSyncQueue(node)) {
  13. LockSupport.park(this);
  14. //中断处理,另一种跳出循环的方式
  15. if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
  16. break;
  17. }
  18. //走到这里说明节点已经条件满足被加入到了同步队列中或者中断了
  19. //这个方法很熟悉吧?就跟独占锁调用同样的获取锁方法,从这里可以看出条件队列只能用于独占锁
  20. if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
  21. interruptMode = REINTERRUPT;
  22. //走到这里说明已经成功获取到了独占锁,接下来就做些收尾工作
  23. //删除条件队列中被取消的节点
  24. if (node.nextWaiter != null)
  25. unlinkCancelledWaiters();
  26. //根据不同模式处理中断
  27. if (interruptMode != 0)
  28. reportInterruptAfterWait(interruptMode);
  29. }

流程比较复杂,一步一步来分析,首先看下加入条件队列的代码:

  1. //注:1.与同步队列不同,条件队列头尾指针是firstWaiter跟lastWaiter
  2. //注:2.条件队列是在获取锁之后,也就是临界区进行操作,因此很多地方不用考虑并发
  3. private Node addConditionWaiter() {
  4. Node t = lastWaiter;
  5. //如果最后一个节点被取消,则删除队列中被取消的节点
  6. //至于为啥是最后一个节点后面会分析
  7. if (t != null && t.waitStatus != Node.CONDITION) {
  8. //删除所有被取消的节点
  9. unlinkCancelledWaiters();
  10. t = lastWaiter;
  11. }
  12. //创建一个类型为CONDITION的节点并加入队列,由于在临界区,所以这里不用并发控制
  13. Node node = new Node(Thread.currentThread(), Node.CONDITION);
  14. if (t == null)
  15. firstWaiter = node;
  16. else
  17. t.nextWaiter = node;
  18. lastWaiter = node;
  19. return node;
  20. }
  21. //删除取消节点的逻辑虽然长,但比较简单,就不单独说了,就是链表删除
  22. private void unlinkCancelledWaiters() {
  23. Node t = firstWaiter;
  24. Node trail = null;
  25. while (t != null) {
  26. Node next = t.nextWaiter;
  27. if (t.waitStatus != Node.CONDITION) {
  28. t.nextWaiter = null;
  29. if (trail == null)
  30. firstWaiter = next;
  31. else
  32. trail.nextWaiter = next;
  33. if (next == null)
  34. lastWaiter = trail;
  35. }
  36. else
  37. trail = t;
  38. t = next;
  39. }
  40. }

把节点加入到条件队列中以后,接下来要做的就是释放锁资源:

  1. //入参就是新创建的节点,即当前节点
  2. final int fullyRelease(Node node) {
  3. boolean failed = true;
  4. try {
  5. //这里这个取值要注意,获取当前的state并释放,这从另一个角度说明必须是独占锁
  6. //可以考虑下这个逻辑放在共享锁下面会发生什么?
  7. int savedState = getState();
  8. //跟独占锁释放锁资源一样,不赘述
  9. if (release(savedState)) {
  10. failed = false;
  11. return savedState;
  12. } else {
  13. //如果这里释放失败,则抛出异常
  14. throw new IllegalMonitorStateException();
  15. }
  16. } finally {
  17. //如果释放锁失败,则把节点取消,由这里就能看出来上面添加节点的逻辑中只需要判断最后一个节点是否被取消就可以了
  18. if (failed)
  19. node.waitStatus = Node.CANCELLED;
  20. }
  21. }

走到这一步,节点也加入条件队列中了,锁资源也释放了,接下来就该挂起了(先忽略中断处理,单看挂起逻辑):

  1. //如果不在同步队列就继续挂起(signal操作会把节点加入同步队列)
  2. while (!isOnSyncQueue(node)) {
  3. LockSupport.park(this);
  4. //中断处理后面再分析
  5. if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
  6. break;
  7. }
  8. //判断节点是否在同步队列中
  9. final boolean isOnSyncQueue(Node node) {
  10. //快速判断1:节点状态或者节点没有前置节点
  11. //注:同步队列是有头节点的,而条件队列没有
  12. if (node.waitStatus == Node.CONDITION || node.prev == null)
  13. return false;
  14. //快速判断2:next字段只有同步队列才会使用,条件队列中使用的是nextWaiter字段
  15. if (node.next != null)
  16. return true;
  17. //上面如果无法判断则进入复杂判断
  18. return findNodeFromTail(node);
  19. }
  20. //注意这里用的是tail,这是因为条件队列中的节点是被加入到同步队列尾部,这样查找更快
  21. //从同步队列尾节点开始向前查找当前节点,如果找到则说明在,否则不在
  22. private boolean findNodeFromTail(Node node) {
  23. Node t = tail;
  24. for (;;) {
  25. if (t == node)
  26. return true;
  27. if (t == null)
  28. return false;
  29. t = t.prev;
  30. }
  31. }

如果被唤醒且已经被转移到了同步队列,则会执行与独占锁一样的方法acquireQueued()进行同步队列独占获取。

最后我们来梳理一下里面的中断逻辑以及收尾工作的代码:

  1. while (!isOnSyncQueue(node)) {
  2. LockSupport.park(this);
  3. //这里被唤醒可能是正常的signal操作也可能是中断
  4. if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
  5. break;
  6. }
  7. //这里的判断逻辑是:
  8. //1.如果现在不是中断的,即正常被signal唤醒则返回0
  9. //2.如果节点由中断加入同步队列则返回THROW_IE,由signal加入同步队列则返回REINTERRUPT
  10. private int checkInterruptWhileWaiting(Node node) {
  11. return Thread.interrupted() ?
  12. (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
  13. 0;
  14. }
  15. //修改节点状态并加入同步队列
  16. //该方法返回true表示节点由中断加入同步队列,返回false表示由signal加入同步队列
  17. final boolean transferAfterCancelledWait(Node node) {
  18. //这里设置节点状态为0,如果成功则加入同步队列
  19. if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
  20. //与独占锁同样的加入队列逻辑,不赘述
  21. enq(node);
  22. return true;
  23. }
  24. //如果上面设置失败,说明节点已经被signal唤醒,由于signal操作会将节点加入同步队列,我们只需自旋等待即可
  25. while (!isOnSyncQueue(node))
  26. Thread.yield();
  27. return false;
  28. }

在把唤醒后的中断判断做好以后,看await()中最后一段逻辑:

  1. //在处理中断之前首先要做的是从同步队列中成功获取锁资源
  2. if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
  3. interruptMode = REINTERRUPT;
  4. //由于当前节点可能是由于中断修改了节点状态,所以如果有后继节点则执行删除已取消节点的操作
  5. //如果没有后继节点,根据上面的分析在后继节点加入的时候会进行删除
  6. if (node.nextWaiter != null)
  7. unlinkCancelledWaiters();
  8. if (interruptMode != 0)
  9. reportInterruptAfterWait(interruptMode);
  10. //根据中断时机选择抛出异常或者设置线程中断状态
  11. private void reportInterruptAfterWait(int interruptMode) throws InterruptedException {
  12. if (interruptMode == THROW_IE)
  13. throw new InterruptedException();
  14. else if (interruptMode == REINTERRUPT)
  15. //实现代码为:Thread.currentThread().interrupt();
  16. selfInterrupt();
  17. }

至此条件队列await操作全部分析完毕。signal()方法相对容易一些,一起看源码分析下:

  1. //条件队列唤醒入口
  2. public final void signal() {
  3. //如果不是独占锁则抛出异常,再次说明条件队列只适用于独占锁
  4. if (!isHeldExclusively())
  5. throw new IllegalMonitorStateException();
  6. //如果条件队列不为空,则进行唤醒操作
  7. Node first = firstWaiter;
  8. if (first != null)
  9. doSignal(first);
  10. }
  11. //该方法就是把一个有效节点从条件队列中删除并加入同步队列
  12. //如果失败则会查找条件队列上等待的下一个节点直到队列为空
  13. private void doSignal(Node first) {
  14. do {
  15. if ( (firstWaiter = first.nextWaiter) == null)
  16. lastWaiter = null;
  17. first.nextWaiter = null;
  18. } while (!transferForSignal(first) &&(first = firstWaiter) != null);
  19. }
  20. //将节点加入同步队列
  21. final boolean transferForSignal(Node node) {
  22. //修改节点状态,这里如果修改失败只有一种可能就是该节点被取消,具体看上面await过程分析
  23. if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
  24. return false;
  25. //该方法很熟悉了,跟独占锁入队方法一样,不赘述
  26. Node p = enq(node);
  27. //注:这里的p节点是当前节点的前置节点
  28. int ws = p.waitStatus;
  29. //如果前置节点被取消或者修改状态失败则直接唤醒当前节点
  30. //此时当前节点已经处于同步队列中,唤醒会进行锁获取或者正确的挂起操作
  31. if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
  32. LockSupport.unpark(node.thread);
  33. return true;
  34. }

三、总结

相比于独占锁跟共享锁,条件队列可能是最不受关注的了,但由于它是阻塞队列实现的关键组件,还是有必要了解一下其中的原理。其实我认为关键点有两条,第一是条件队列是建立在某个具体的锁上面的,第二是条件队列跟同步队列是两个队列,前者依赖条件唤醒后者依赖锁释放唤醒,了解了这两点以后搞清楚条件队列就不是什么难事了。


至此,Java同步器AQS中三大锁模式就都分析完了。虽然已经尽力思考,尽量写的清楚,但鉴于水平有限,如果有纰漏的地方,欢迎广大读者指正。

明天就是国庆长假了,我自己也计划出国玩一趟,散散心。

提前祝广大朋友国庆快乐。

深入浅出AQS之条件队列的更多相关文章

  1. 深入浅出AQS之组件概览

    之前分析了AQS中的独占锁,共享锁,条件队列三大模块,现在从结构上来看看AQS各个组件的情况. 原文地址:http://www.jianshu.com/p/49b86f9cd7ab 深入浅出AQS之独 ...

  2. 母鸡下蛋实例:多线程通信生产者和消费者wait/notify和condition/await/signal条件队列

    简介 多线程通信一直是高频面试考点,有些面试官可能要求现场手写生产者/消费者代码来考察多线程的功底,今天我们以实际生活中母鸡下蛋案例用代码剖析下实现过程.母鸡在鸡窝下蛋了,叫练从鸡窝里把鸡蛋拿出来这个 ...

  3. 《java并发编程实战》读书笔记11--构建自定义的同步工具,条件队列,Condition,AQS

    第14章 构建自定义的同步工具 本章将介绍实现状态依赖性的各种选择,以及在使用平台提供的状态依赖机制时需要遵守的各项规则. 14.1 状态依赖性的管理 对于并发对象上依赖状态的方法,虽然有时候在前提条 ...

  4. 看看AQS阻塞队列和条件队列

    上一篇简单介绍了AQS,我们大概知道AQS就是一个框架,把很多功能都给实现了(比如入队规则,唤醒节点中的线程等),我们如果要使用的话只需要实现其中的一些方法(比如tryAcquire等)就行了!这次主 ...

  5. AQS源码深入分析之条件队列-你知道Java中的阻塞队列是如何实现的吗?

    本文基于JDK-8u261源码分析 1 简介 因为CLH队列中的线程,什么线程获取到锁,什么线程进入队列排队,什么线程释放锁,这些都是不受我们控制的.所以条件队列的出现为我们提供了主动式地.只有满足指 ...

  6. Java高级:条件队列与同步器Synchronizer的原理+AQS的应用

    14.构建自定义的同步工具 类库中包含了许多存在状态依赖性的类,例如FutureTask,Semaphore和BlockingQueue等.在这些类中的一些操作中有着基于状态的前提条件,例如,不能从一 ...

  7. 从零开始了解多线程 之 深入浅出AQS -- 上

    java锁&AQS深入浅出学习--上 上一篇文章中我们一起学习了jvm缓存一致性.多线程间的原子性.有序性.指令重排的相关内容, 这一篇文章便开始和大家一起学习学习AQS(AbstractQu ...

  8. 深入浅出AQS之独占锁模式

    每一个Java工程师应该都或多或少了解过AQS,我自己也是前前后后,反反复复研究了很久,看了忘,忘了再看,每次都有不一样的体会.这次趁着写博客,打算重新拿出来系统的研究下它的源码,总结成文章,便于以后 ...

  9. 深入浅出AQS之共享锁模式

    在了解了AQS独占锁模式以后,接下来再来看看共享锁的实现原理. 原文地址:http://www.jianshu.com/p/1161d33fc1d0 搞清楚AQS独占锁的实现原理之后,再看共享锁的实现 ...

随机推荐

  1. MongoDB分布式集群搭建(分片加副本集)

    # 环境准备 服务器 # 环境搭建 文件配置和目录添加 新建目录的操作要在三台机器中进行,为配置服务器新建数据目录和日志目录 mkdir -p $MONGODB_HOME/config/data mk ...

  2. 斐波那契数列—Java

    斐波那契数列想必大家都知道吧,如果不知道的话,我就再啰嗦一遍, 斐波那契数列为:1 2 3 5 8 13 ...,也就是除了第一项和第二项为1以外,对于第N项,有f(N)=f(N-1)+f(N-2). ...

  3. 持续交付Jenkins使用

    简介 Jenkins是一个独立的开源自动化服务器,可用于自动化各种任务,如构建,测试和部署软件.Jenkins可以通过本机系统包Docker安装,甚至可以通过安装Java Runtime Enviro ...

  4. Python Keras module 'keras.backend' has no attribute 'image_data_format'

    问题: 当使用Keras运行示例程序mnist_cnn时,出现如下错误: 'keras.backend' has no attribute 'image_data_format' 程序路径https: ...

  5. java 多线程(0) Java线程

    线程 线程是系统调度的基本单元,每当创建一个进程时,会有许多的线程,也叫轻量级进程,在一个进程中拥有多个线程,各自都有自己的计数器,堆和局部变量属性,并且能够分享内存变量. 为什么要使用多线程  1. ...

  6. CentOS 6.5安装部署Zabbix监控系统

    CentOS 6.5安装部署Zabbix监控系统 先说一点废话,我没有用centos7做实验,讲真,centos 7我也不常用,喜欢新版本的同学其实可以尝试下,注意一点的就是centos 6.5只支持 ...

  7. 2nd_SE-结对编程1-基于flask框架的四则运算生成器

    0x00 Coding https://coding.net/u/nikochan/p/2nd_SE/git 0x01 写在前面 因为在上一个作业中,是基于python完成的Command程序.那么再 ...

  8. 【Alpha阶段】第四次scrum meeting

    一.会议照片 二.会议内容 姓名 学号 负责模块 昨日任务完成度 今日任务 杨爱清 099 界面设计和交互功能 完成 百度合适的背景图片 杨立鑫 100 数据库搭建和其他 完成 开始编辑数据库 林 钊 ...

  9. 201521123064 《Java程序设计》第3周学习总结

    1. 本章学习总结 2. 书面作业 Q1:代码阅读 public class Test1 { private int i = 1;//这行不能修改 private static int j = 2; ...

  10. 201521123038 《Java程序设计》 第三周学习总结

    201521123038 <Java程序设计> 第三周学习总结 1. 本周学习总结 附大图链接 http://naotu.baidu.com/file/5774caa2be710afbc0 ...