一、引言

一般我们在使用锁的Condition时,我们一般都是这么使用,以ReentrantLock为例,

  1. ReentrantLock lock = new ReentrantLock();
  2. Condition condition = lock.newCondition();
  3.  
  4. lock.lock();
  5. try{
  6. condition.await();
  7.  
  8. }finally{
  9.  
  10. lock.unlock();
  11. }
  12.  
  13. lock.lock();
  14. try{
  15. condition.signal();
  16.  
  17. }finally{
  18.  
  19. lock.unlock();
  20. }

  从上面可以知道,我们调用Condition的await和signal方法必须是在获取得到锁的情况下,首先我们以这个为基础,先不管是如何获取得到锁的,那么上面的程序在condition.await()时阻塞当前调用的线程,而调用 condition.signal()方法的时候可能唤起一个正在await阻塞的线程,我这里说的是可能不是一定。为什么这么说,我们来看下await()方法主要做了什么事情。

二、分析

下面是await 方法的在jdk8的源码,

  1. 下面是await 方法的在jdk8的源码,
  2. public final void await() throws InterruptedException {
  3. if (Thread.interrupted()) // ①
  4. throw new InterruptedException();
  5. Node node = addConditionWaiter(); //②
  6. int savedState = fullyRelease(node); //③
  7. int interruptMode = 0;
  8. while (!isOnSyncQueue(node)) { //④
  9. LockSupport.park(this);
  10. if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) // ⑤
  11. break;
  12. }
  13. if (acquireQueued(node, savedState) && interruptMode != THROW_IE) //⑥
  14. interruptMode = REINTERRUPT;
  15. if (node.nextWaiter != null) // clean up if cancelled ⑦
  16. unlinkCancelledWaiters();
  17. if (interruptMode != 0) // ⑧
  18. reportInterruptAfterWait(interruptMode);
  19. }

执行过程如下,
① ,判断当前显示是否被interrupt? 是的话,会抛出中断异常InterruptedException,
② ,调用addConditionWaiter方法,主要是new 一个以当前线程为数据的节点Node,然后添加到condition条件队列里。
③ ,调用fullyRelease方法,该方法内部调用release方法,而release方法主要是在AQS队列里面唤起第一个节点(即head的next节点)的线程(如果head后继节点存在的话)。这时,在ASQ同步器内至少有2个活动的线程(一个是当前线程(可能是头节点),另一个是唤起的线程(如果存在))。如果唤起失败,会抛异常IllegalMonitorStateException。注:当存在唤起的线程的时候,这个线程就可以去争取获取锁。
④ ,通过isOnSyncQueue方法判断该节点是否在AQS队列中,当调用await时候,肯定不在AQS上,因为addConditionWaiter方法是new 一个新的Node.接着会进入while循环里面。调用 LockSupport.park(this);阻塞当前线程,相当于释放锁。
⑤ ,当当前线程被唤起的时候(可能是 另一个await线程唤起的第一个节点可能是这个线程,或者在signal下,前驱节点已经cancel时,第一个firstWaiter节点是该当前节点),需要判断是否被中断,存储在interruptMode,如果被中断则break,否则checkInterruptWhileWaiting返回0,那么会接着判断node节点是否在AQS中,如果还是不在的话,park当前线程,否则跳出while循环。那么node节点是什么时候被加入到AQS上的,答案是在signal方法上。
⑥ ,当node节点在AQS队列时,我们需要获取锁,只有当前线程的节点Node在AQS队列上,才能去争取锁。争取锁就是通过调用acquireQueued方法。等下来分析下acquireQueued方法。
⑦ , 如果当前节点的node.nextWaiter不为空,说明还有其他线程在该condition上,并且当前的线程已经获取锁,接着清除条件队列上的cancel类型节点
⑧ , 如果interruptMode 是InterruptedException类型或者REINTERRUPT类型。则进行相应的抛中断异常或者线程自我中断标志位设置。

接着,来分析下signal方法

  1. public final void signal() {
  2. if (!isHeldExclusively()) // ①
  3. throw new IllegalMonitorStateException();
  4. Node first = firstWaiter;
  5. if (first != null)
  6. doSignal(first); // ②
  7. }

执行过程如下,
① ,如果当前线程不是持有该condition的锁,那么执行抛IllegalMonitorStateException异常。
② ,调用doSignal方法,并且条件队列的首节点传入。

  1. private void doSignal(Node first) {
  2. do {
  3. if ( (firstWaiter = first.nextWaiter) == null) // ①
  4. lastWaiter = null;
  5. first.nextWaiter = null;
  6. } while (!transferForSignal(first) && (first = firstWaiter) != null); // ②
  7. }

 

① ,这种条件队列firstWaiter指针为next节点,因为当前的节点需要被移除条件队列,并且next节点为空,那么lastWaiter置为null,说明是空条件队列,接着把first.nextWaiter=null,说明移除了条件队列
② ,在这里有2步操作,一是transferForSignal,二是 first = firstWaiter,如果我们当前first节点入AQS队列成功,那么transferForSignal返回true,则doSignal的while循环结束,
如果当前的first节点入AQS返回失败,则需要next的节点重新signal,保证有一个成功的firstWaiter节点入AQS队列。接着来看下transferForSignal 方法主要做了什么事情。

  1. final boolean transferForSignal(Node node) {
  2. /*
  3. * If cannot change waitStatus, the node has been cancelled.
  4. */
  5. if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) // ①
  6. return false;
  7.  
  8. /*
  9. * Splice onto queue and try to set waitStatus of predecessor to
  10. * indicate that thread is (probably) waiting. If cancelled or
  11. * attempt to set waitStatus fails, wake up to resync (in which
  12. * case the waitStatus can be transiently and harmlessly wrong).
  13. */
  14. Node p = enq(node); // ②
  15. int ws = p.waitStatus;
  16. if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) // ③
  17. LockSupport.unpark(node.thread);
  18. return true;
  19. }

① ,首先传入的节点node是条件队列的第一个节点(在外部已经移除),改变其状态CONDITION,为初始状态0,如果改变失败,说明该节点已经不是条件节点,直接返回false,doSignal方法重新调用新firstWaiter节点入AQS队列,
② ,把首节点入AQS节点,enq()方法返回的是入节点的前驱节点。从这里核心方法可以知道,signal() 方法的作用其实只是把等待队列中第一个非取消节点转移到AQS的同步队列尾部。转移后的节点很可能正在在同步队列阻塞着,什么时候唤醒,取决于它的前驱节点是否是头节点。
③ ,如果当前前驱节点的waitStatus>0(说明是CANCELLED状态),前驱节点已经Canncel(说明前驱节点已经中断等情况),则可以调用LockSupport.unpark(node.thread)唤起线程,则await方法的park返回可以立即返回,预先将AQS同步队列中取消的节点移除掉,而不用等到获取同步状态失败的时候再去判断了,起到一定的优化作用。

最后来分析下获取锁方法 acquireQueued

执行如下:

  1. final boolean acquireQueued(final Node node, int arg) { // ①
  2. boolean failed = true;
  3. try {
  4. boolean interrupted = false;
  5. for (;;) {
  6. final Node p = node.predecessor();
  7. if (p == head && tryAcquire(arg)) { //②
  8. setHead(node);
  9. p.next = null; // help GC
  10. failed = false;
  11. return interrupted;
  12. }
  13. if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) // ③
  14. interrupted = true;
  15. }
  16. } finally {
  17. if (failed)
  18. cancelAcquire(node);
  19. }
  20. }

① ,传入node 为需要获取锁的节点,arg为之前state状态.
② ,如果传入的节点的前驱节点是head节点,说明当前节点是AQS队列的首节点,可以尝试去获取锁,即我们需要是要实现的同步语义方法tryAcquire,如果同步语义获取锁成功,则设置当前头节点为头节点。
这里注意返回的值得语义是是否发生中断,而不是获取锁是否成功。
③ ,调用 shouldParkAfterFailedAcquire方法,该方法用来判断获取锁失败后是否需要park当前线程,如果需要park线程,则接着判断该线程是否有中断标志。

接着我们来看下shouldParkAfterFailedAcquire 方法。

  1. private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
  2. int ws = pred.waitStatus;
  3. if (ws == Node.SIGNAL) // ①
  4. /*
  5. * This node has already set status asking a release
  6. * to signal it, so it can safely park.
  7. */
  8. return true;
  9. if (ws > 0) { // ②
  10. /*
  11. * Predecessor was cancelled. Skip over predecessors and
  12. * indicate retry.
  13. */
  14. do {
  15. node.prev = pred = pred.prev;
  16. } while (pred.waitStatus > 0);
  17. pred.next = node;
  18. } else {
  19. /*
  20. * waitStatus must be 0 or PROPAGATE. Indicate that we
  21. * need a signal, but don't park yet. Caller will need to
  22. * retry to make sure it cannot acquire before parking.
  23. */
  24. compareAndSetWaitStatus(pred, ws, Node.SIGNAL); //③
  25. }
  26. return false;
  27. }

① 前驱节点pred的waitStatus为SIGNAL,说明当前节点有效,返回true,代表需要park当前线程,
② 前驱节点已经取消,则删除去取消掉的前驱节点,返回false,外面继续for循环获取锁
③ 处在该条件语句的前驱节点的waitStatus必定是 0,或者是传播PROPAGATE,则设置传播节点为SIGNAL,然后返回false,则接着去for循环获取锁,并且失败的时候,调用shouldParkAfterFailedAcquire时知道前驱为SIGNAL(之前由③设置),则需要park线程。
从这里可以知道transferForSignal方法中,!compareAndSetWaitStatus(p, ws, Node.SIGNAL)语句,如果对其前驱节点设置Node.SIGNAL失败,则不需要等到acquireQueued去判断是否需要park线程,直接unpark线程即可

AQS之Condition的更多相关文章

  1. AbstractQueuedSynchronizer源码解读--续篇之Condition

    1. 背景 在之前的AbstractQueuedSynchronizer源码解读中,介绍了AQS的基本概念.互斥锁.共享锁.AQS对同步队列状态流转管理.线程阻塞与唤醒等内容.其中并不涉及Condit ...

  2. Java并发编程系列-(4) 显式锁与AQS

    4 显示锁和AQS 4.1 Lock接口 核心方法 Java在java.util.concurrent.locks包中提供了一系列的显示锁类,其中最基础的就是Lock接口,该接口提供了几个常见的锁相关 ...

  3. JAVA并发-Condition

    简介 在没有Lock之前,我们使用synchronized来控制同步,配合Object的wait().notify()系列方法可以实现等待/通知模式.在Java SE5后,Java提供了Lock接口, ...

  4. 面经手册 · 第17篇《码农会锁,ReentrantLock之AQS原理分析和实践使用》

    作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 如果你相信你做什么都能成,你会自信的多! 千万不要总自我否定,尤其是职场的打工人.如 ...

  5. Java并发包源码学习系列:详解Condition条件队列、signal和await

    目录 Condition接口 AQS条件变量的支持之ConditionObject内部类 回顾AQS中的Node void await() 添加到条件队列 Node addConditionWaite ...

  6. AbstractQueuedSynchronizer源码解读

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

  7. 线程池ThreadPoolExecutor源码分析

    在阿里编程规约中关于线程池强制了两点,如下: [强制]线程资源必须通过线程池提供,不允许在应用中自行显式创建线程.说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源 ...

  8. 给JDK提的一个bug(关于AbstractQueuedSynchronizer.ConditionObject)

    1. 背景 之前读JUC的AQS源码,读到Condition部分,我当时也写了一篇源码阅读文章--(AbstractQueuedSynchronizer源码解读--续篇之Condition)[http ...

  9. ReentrantLock 学习笔记

    有篇写的很不错的博客:https://blog.csdn.net/aesop_wubo/article/details/7555956    基于JDK1.8 参考着看源码 ,弄清楚lock()和un ...

随机推荐

  1. springboot 使用redis

    安装redis教程:https://www.cnblogs.com/nongzihong/p/10190489.html 依赖: <!--配置redis--> <dependency ...

  2. Mysql 中需不需要commit

    摘自:https://blog.csdn.net/zzyly1/article/details/81003122 mysql在进行增删改操作的时候需不需要commit,这得看你的存储引擎, 如果是不支 ...

  3. spark 笔记 13: 再看DAGScheduler,stage状态更新流程

    当某个task完成后,某个shuffle Stage X可能已完成,那么就可能会一些仅依赖Stage X的Stage现在可以执行了,所以要有响应task完成的状态更新流程. ============= ...

  4. C++ Map实践

    实践如下: #include <iostream> #include <map> #include <string> #include <typeinfo&g ...

  5. Eclipse如何安装Fat Jar

    〇.安装前准备 1.Fat Jar插件下载地址:https://sourceforge.net/projects/fjep/files/ 2.安装前请确认Eclipse版本:Help --> A ...

  6. Reactjs之静态路由、动态路由以及Get传值以及获取

    1.新增知识点 /* react路由的配置: 1.找到官方文档 https://reacttraining.com/react-router/web/example/basic 2.安装 cnpm i ...

  7. runoob_Java 序列化

    Java 序列化 Java 提供了一种对象序列化的机制,该机制中,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据.有关对象的类型的信息和存储在对象中数据的类型. 将序列化对象写入文件之后 ...

  8. sha256---利用java自带的实现加密

    利用java自带的实现加密:参考https://jingyan.baidu.com/article/2fb0ba40a2ef2b00f3ec5f74.html /** * 利用java原生的摘要实现S ...

  9. java:maven(maven-ssm(聚合,分包开发))

    1.maven-ssm: maven-ssm_diy: pom.xml: <?xml version="1.0" encoding="UTF-8"?> ...

  10. elasticsearch head + xpack 用户名密码访问

    修改配置文件elasticsearch.yml,增加http.cors.allow-headers: Authorization 访问head时,url如下所示:http://192.168.100. ...