简介

在没有Lock之前,我们使用synchronized来控制同步,配合Object的wait()、notify()系列方法可以实现等待/通知模式。在Java SE5后,Java提供了Lock接口,相对于Synchronized而言,Lock提供了条件Condition,对线程的等待、唤醒操作更加详细和灵活。下图是Condition与Object的监视器方法的对比(摘自《Java并发编程的艺术》):

Condition提供了一系列的方法来对阻塞和唤醒线程:

  1. await() :造成当前线程在接到信号或被中断之前一直处于等待状态。
  2. await(long time, TimeUnit unit) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
  3. awaitNanos(long nanosTimeout) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。返回值表示剩余时间,如果在nanosTimesout之前唤醒,那么返回值 = nanosTimeout - 消耗时间,如果返回值 <= 0 ,则可以认定它已经超时了。
  4. awaitUninterruptibly() :造成当前线程在接到信号之前一直处于等待状态。【注意:该方法对中断不敏感】。
  5. awaitUntil(Date deadline) :造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。如果没有到指定时间就被通知,则返回true,否则表示到了指定时间,返回返回false。
  6. signal() :唤醒一个等待线程。该线程从等待方法返回前必须获得与Condition相关的锁。
  7. signal()All :唤醒所有等待线程。能够从等待方法返回的线程必须获得与Condition相关的锁。

Condition是一种广义上的条件队列。他为线程提供了一种更为灵活的等待/通知模式,线程在调用await方法后执行挂起操作,直到线程等待的某个条件为真时才会被唤醒。Condition必须要配合锁一起使用,因为对共享状态变量的访问发生在多线程环境下。一个Condition的实例必须与一个Lock绑定,因此Condition一般都是作为Lock的内部实现。

Condtion的实现

获取一个Condition必须要通过Lock的newCondition()方法。该方法定义在接口Lock下面,返回的结果是绑定到此 Lock 实例的新 Condition 实例。Condition为一个接口,其下仅有一个实现类ConditionObject,由于Condition的操作需要获取相关的锁,而AQS则是同步锁的实现基础,所以ConditionObject则定义为AQS的内部类。定义如下:

  1. public class ConditionObject implements Condition, java.io.Serializable {
  2. }

等待队列

每个Condition对象都包含着一个FIFO队列,该队列是Condition对象通知/等待功能的关键。在队列中每一个节点都包含着一个线程引用,该线程就是在该Condition对象上等待的线程。我们看Condition的定义就明白了:

  1. public class ConditionObject implements Condition, java.io.Serializable {
  2. private static final long serialVersionUID = 1173984872572414699L;
  3. //头节点
  4. private transient Node firstWaiter;
  5. //尾节点
  6. private transient Node lastWaiter;
  7. public ConditionObject() {
  8. }
  9. /** 省略方法 **/
  10. }

从上面代码可以看出Condition拥有首节点(firstWaiter),尾节点(lastWaiter)。当前线程调用await()方法,将会以当前线程构造成一个节点(Node),并将节点加入到该队列的尾部。结构如下:

Node里面包含了当前线程的引用。Node定义与AQS的CLH同步队列的节点使用的都是同一个类(AbstractQueuedSynchronized.Node静态内部类)。

Condition的队列结构比CLH同步队列的结构简单些,新增过程较为简单只需要将原尾节点的nextWaiter指向新增节点,然后更新lastWaiter即可。

等待await

调用Condition的await()方法会使当前线程进入等待状态,同时会加入到Condition等待队列同时释放锁。当从await()方法返回时,当前线程一定是获取了Condition相关连的锁。

  1. public final void await() throws InterruptedException {
  2. // 当前线程中断
  3. if (Thread.interrupted())
  4. throw new InterruptedException();
  5. //当前线程加入等待队列
  6. Node node = addConditionWaiter();
  7. //释放锁
  8. long savedState = fullyRelease(node);
  9. int interruptMode = 0;
  10. /**
  11. * 检测此节点的线程是否在同步队上,如果不在,则说明该线程还不具备竞争锁的资格,则继续等待
  12. * 直到检测到此节点在同步队列上
  13. */
  14. while (!isOnSyncQueue(node)) {
  15. //线程挂起
  16. LockSupport.park(this);
  17. //如果已经中断了,则退出
  18. if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
  19. break;
  20. }
  21. //竞争同步状态
  22. if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
  23. interruptMode = REINTERRUPT;
  24. //清理下条件队列中的不是在等待条件的节点
  25. if (node.nextWaiter != null) // clean up if cancelled
  26. unlinkCancelledWaiters();
  27. if (interruptMode != 0)
  28. reportInterruptAfterWait(interruptMode);
  29. }

此段代码的逻辑是:首先将当前线程新建一个节点同时加入到条件队列中,然后释放当前线程持有的同步状态。然后则是不断检测该节点代表的线程释放出现在CLH同步队列中(收到signal信号之后就会在AQS队列中检测到),如果不存在则一直挂起,否则参与竞争同步状态。

加入条件队列(addConditionWaiter())源码如下:

  1. private Node addConditionWaiter() {
  2. Node t = lastWaiter; //尾节点
  3. //Node的节点状态如果不为CONDITION,则表示该节点不处于等待状态,需要清除节点
  4. if (t != null && t.waitStatus != Node.CONDITION) {
  5. //清除条件队列中所有状态不为Condition的节点
  6. unlinkCancelledWaiters();
  7. t = lastWaiter;
  8. }
  9. //当前线程新建节点,状态CONDITION
  10. Node node = new Node(Thread.currentThread(), Node.CONDITION);
  11. /**
  12. * 将该节点加入到条件队列中最后一个位置
  13. */
  14. if (t == null)
  15. firstWaiter = node;
  16. else
  17. t.nextWaiter = node;
  18. lastWaiter = node;
  19. return node;
  20. }

该方法主要是将当前线程加入到Condition条件队列中。当然在加入到尾节点之前会清楚所有状态不为Condition的节点。

fullyRelease(Node node),负责释放该线程持有的锁。

  1. final long fullyRelease(Node node) {
  2. boolean failed = true;
  3. try {
  4. //节点状态--其实就是持有锁的数量
  5. long savedState = getState();
  6. //释放锁
  7. if (release(savedState)) {
  8. failed = false;
  9. return savedState;
  10. } else {
  11. throw new IllegalMonitorStateException();
  12. }
  13. } finally {
  14. if (failed)
  15. node.waitStatus = Node.CANCELLED;
  16. }
  17. }

isOnSyncQueue(Node node):如果一个节点刚开始在条件队列上,现在在同步队列上获取锁则返回true

  1. final boolean isOnSyncQueue(Node node) {
  2. //状态为Condition,获取前驱节点为null,返回false
  3. if (node.waitStatus == Node.CONDITION || node.prev == null)
  4. return false;
  5. //后继节点不为null,肯定在CLH同步队列中
  6. if (node.next != null)
  7. return true;
  8. return findNodeFromTail(node);
  9. }

unlinkCancelledWaiters():负责将条件队列中状态不为Condition的节点删除

  1. private void unlinkCancelledWaiters() {
  2. Node t = firstWaiter;
  3. Node trail = null;
  4. while (t != null) {
  5. Node next = t.nextWaiter;
  6. if (t.waitStatus != Node.CONDITION) {
  7. t.nextWaiter = null;
  8. if (trail == null)
  9. firstWaiter = next;
  10. else
  11. trail.nextWaiter = next;
  12. if (next == null)
  13. lastWaiter = trail;
  14. }
  15. else
  16. trail = t;
  17. t = next;
  18. }
  19. }

通知signal

调用Condition的signal()方法,将会唤醒在等待队列中等待最长时间的节点(条件队列里的首节点),在唤醒节点前,会将节点移到CLH同步队列中。

  1. public final void signal() {
  2. //检测当前线程是否为拥有锁的独
  3. if (!isHeldExclusively())
  4. throw new IllegalMonitorStateException();
  5. //头节点,唤醒条件队列中的第一个节点
  6. Node first = firstWaiter;
  7. if (first != null)
  8. doSignal(first); //唤醒
  9. }

该方法首先会判断当前线程是否已经获得了锁,这是前置条件。然后唤醒条件队列中的头节点。

doSignal(Node first):唤醒头节点

  1. private void doSignal(Node first) {
  2. do {
  3. //修改头结点,完成旧头结点的移出工作
  4. if ( (firstWaiter = first.nextWaiter) == null)
  5. lastWaiter = null;
  6. first.nextWaiter = null;
  7. } while (!transferForSignal(first) &&
  8. (first = firstWaiter) != null);
  9. }

doSignal(Node first)主要是做两件事:1.修改头节点,2.调用transferForSignal(Node first) 方法将节点移动到CLH同步队列中。transferForSignal(Node first)源码如下:

  1. final boolean transferForSignal(Node node) {
  2. //将该节点从状态CONDITION改变为初始状态0,
  3. if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
  4. return false;
  5. //将节点加入到syn队列中去,返回的是syn队列中node节点前面的一个节点
  6. Node p = enq(node);
  7. int ws = p.waitStatus;
  8. //如果结点p的状态为cancel 或者修改waitStatus失败,则直接唤醒
  9. if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
  10. LockSupport.unpark(node.thread);
  11. return true;
  12. }

整个通知的流程如下:

  1. 判断当前线程是否已经获取了锁,如果没有获取则直接抛出异常,因为获取锁为通知的前置条件。
  2. 如果线程已经获取了锁,则将唤醒条件队列的首节点
  3. 唤醒首节点是先将条件队列中的头节点移出,然后调用AQS的enq(Node node)方法将其安全地移到CLH同步队列中
  4. 最后判断如果该节点的同步状态是否为Cancel,或者修改状态为Signal失败时,则直接调用LockSupport唤醒该节点的线程。

总结

等待队列:即ConditionObject构建的Node单向链表队列,一个lock可以有多个等待队列

同步队列:即AQS内部类Node构建的FIFO的双向链表队列,也叫CLH同步队列,一个lock只能有一个同步队列

一个线程获取锁后,通过调用Condition的await()方法,会将当前线程先加入到条件队列中,然后释放锁,最后通过isOnSyncQueue(Node node)方法不断自检看节点是否已经在CLH同步队列了,如果是则尝试获取锁,否则一直挂起。当线程调用signal()方法后,程序首先检查当前线程是否获取了锁,然后通过doSignal(Node first)方法唤醒等待队列的首节点。被唤醒的线程,将从await()方法中的while循环中退出来,然后调用acquireQueued()方法竞争同步状态。

图1

图2

上面这个gif动画中,等待队列,Condition队列描述有问题,其他挺好的。

图3

应用

下面是JAVA官方提供的生产消费模式案例:

  1. import java.util.concurrent.locks.Condition;
  2. import java.util.concurrent.locks.Lock;
  3. import java.util.concurrent.locks.ReentrantLock;
  4. public class Main {
  5. public static void main(String[] args) {
  6. final BoundedBuffer boundedBuffer = new BoundedBuffer();
  7. Thread t1 = new Thread(new Runnable() {
  8. @Override
  9. public void run() {
  10. System.out.println("t1 run");
  11. for (int i=0;i<20;i++) {
  12. try {
  13. System.out.println("putting..");
  14. boundedBuffer.put(Integer.valueOf(i));
  15. } catch (InterruptedException e) {
  16. e.printStackTrace();
  17. }
  18. }
  19. }
  20. }) ;
  21. Thread t2 = new Thread(new Runnable() {
  22. @Override
  23. public void run() {
  24. for (int i=0;i<20;i++) {
  25. try {
  26. Object val = boundedBuffer.take();
  27. System.out.println(val);
  28. } catch (InterruptedException e) {
  29. e.printStackTrace();
  30. }
  31. }
  32. }
  33. }) ;
  34. t1.start();
  35. t2.start();
  36. }
  37. /**
  38. * BoundedBuffer 是一个定长100的集合,当集合中没有元素时,take方法需要等待,直到有元素时才返回元素
  39. * 当其中的元素数达到最大值时,要等待直到元素被take之后才执行put的操作
  40. * @author yukaizhao
  41. *
  42. */
  43. static class BoundedBuffer {
  44. final Lock lock = new ReentrantLock();
  45. final Condition notFull = lock.newCondition();
  46. final Condition notEmpty = lock.newCondition();
  47. final Object[] items = new Object[100];
  48. int putptr, takeptr, count;
  49. public void put(Object x) throws InterruptedException {
  50. System .out.println("put wait lock");
  51. lock.lock();
  52. System.out.println("put get lock");
  53. try {
  54. while (count == items.length) {
  55. System.out.println("buffer full, please wait");
  56. notFull.await();
  57. }
  58. items[putptr] = x;
  59. if (++putptr == items.length)
  60. putptr = 0;
  61. ++count;
  62. notEmpty.signal();
  63. } finally {
  64. lock.unlock();
  65. }
  66. }
  67. public Object take() throws InterruptedException {
  68. System.out.println("take wait lock");
  69. lock.lock();
  70. System.out.println("take get lock");
  71. try {
  72. while (count == 0) {
  73. System.out.println("no elements, please wait");
  74. notEmpty.await();
  75. }
  76. Object x = items[takeptr];
  77. if (++takeptr == items.length)
  78. takeptr = 0;
  79. --count;
  80. notFull.signal();
  81. return x;
  82. } finally {
  83. lock.unlock();
  84. }
  85. }
  86. }
  87. }

参考:

【死磕Java并发】—–J.U.C之Condition

Java 并发编程 J.U.C 之 Condition

Java锁详解(二)------ LockSupport 与 Condition

AQS之Condition实现分析

深入浅出-AQS的Condition实现原理

深入剖析基于并发AQS的(独占锁)重入锁(ReetrantLock)及其Condition实现原理

JAVA并发-Condition的更多相关文章

  1. Java并发Condition接口

    java.util.concurrent.locks.Condition接口提供一个线程挂起执行的能力,直到给定的条件为真. Condition对象必须绑定到Lock,并使用newCondition( ...

  2. 【Java并发系列04】线程锁synchronized和Lock和volatile和Condition

    img { border: solid 1px } 一.前言 多线程怎么防止竞争资源,即防止对同一资源进行并发操作,那就是使用加锁机制.这是Java并发编程中必须要理解的一个知识点.其实使用起来还是比 ...

  3. Java并发编程:线程间协作的两种方式:wait、notify、notifyAll和Condition

    Java并发编程:线程间协作的两种方式:wait.notify.notifyAll和Condition 在前面我们将了很多关于同步的问题,然而在现实中,需要线程之间的协作.比如说最经典的生产者-消费者 ...

  4. java并发编程——通过ReentrantLock,Condition实现银行存取款

         java.util.concurrent.locks包为锁和等待条件提供一个框架的接口和类,它不同于内置同步和监视器.该框架允许更灵活地使用锁和条件,但以更难用的语法为代价. Lock 接口 ...

  5. java并发多线程显式锁Condition条件简介分析与监视器 多线程下篇(四)

    Lock接口提供了方法Condition newCondition();用于获取对应锁的条件,可以在这个条件对象上调用监视器方法 可以理解为,原本借助于synchronized关键字以及锁对象,配备了 ...

  6. Java并发编程原理与实战二十二:Condition的使用

    Condition的使用 Condition用于实现条件锁,可以唤醒指定的阻塞线程.下面来实现一个多线程顺序打印a,b,c的例子. 先来看用wait和notify的实现: public class D ...

  7. 19、Java并发编程:线程间协作的两种方式:wait、notify、notifyAll和Condition

    Java并发编程:线程间协作的两种方式:wait.notify.notifyAll和Condition 在前面我们将了很多关于同步的问题,然而在现实中,需要线程之间的协作.比如说最经典的生产者-消费者 ...

  8. Java并发(十一):Condition条件

    先做总结: 1.为什么使用Condition条件? synchronized配合Object的wait().notify()系列方法可以实现等待/通知模式. Lock提供了条件Condition,对线 ...

  9. 006 Java并发编程wait、notify、notifyAll和Condition

    原文https://www.cnblogs.com/dolphin0520/p/3920385.html#4182690 Java并发编程:线程间协作的两种方式:wait.notify.notifyA ...

随机推荐

  1. pytest--配置

    说到配置,大家可能想到的是不经常更改的内容,比如Django里的settings.py文件,或者我们做自动化的时候,把测试环境的域名和正式环境的域名放到一个配置文件里,所有的接口都从这个文件里读取.这 ...

  2. 认识一下transition

    transition 以前的CSS属性切换时,由于只有开始和截止两个状态,切换时略显生硬 jquery.animate 传说中的jquery在保证兼容性之后,又为开发者提供了简洁的过渡(动画其中之一效 ...

  3. mybatis+postgresql10插入返回主键ID

    MyBatis如果使用useGeneratedKeys去生成自增列会造成不成功,因为官方提供只支持这些数据库:mybatis generatedkeys,那么如果要用在oracle和postgresq ...

  4. 使用 jQuery.AutoComplete 让文本框自动完成

    直接贴代码了. @section headSection { <script type="text/javascript"> $(document).ready(fun ...

  5. IDA分析时添加新的C语言结构体

    View - Open Subviews - Local Type - INSERT键 - 输入新结构体 - 右击"Synchornize to idb" 之后再分析处按 T 就可 ...

  6. mysql error 1364 Field doesn't have a default values

    https://stackoverflow.com/questions/15438840/mysql-error-1364-field-doesnt-have-a-default-values. us ...

  7. c# 调用接口返回json

    需要命名空间 using System.Net; using System.Net.Security using System.Security.Cryptography.X509Certificat ...

  8. .net怎么使用Swagger

    目录导航 一.安装 二.配置 三.调用 四.错误记录 一.安装 新建一个没有身份验证的mvc项目 - SwaggerMvc5Demo,然后添加一个名为Remote(自定义)且包含基础读写(不想手写)的 ...

  9. python 提取整个 HTML 节点

    有的时候,需要把整个 HTML 节点原封不动地取下来,也就是包括节点标签.节点内容,甚至也包括内容中的空格.各种特殊符号等等. 假设已获取到页面源码,并将其保存在变量 src 中.则可有代码如下: f ...

  10. python3模块

    一.sys模块 import sys #print(sys.path) #打印环境变量 #print(sys.argv) print(sys.argv[3]) Sys.argv[ ]其实就是一个列表, ...