关于AQS等待/唤醒机制:ConditionObject的原理分析

在上一篇文章 认真的并发基石:AQS源码分析与图解 中,我们留了个小尾巴,没对ConditionObject进行源码分析,这一篇我们补上。

说明: 本篇不再啰嗦AQS相关知识,因为我们上一篇都讲过了,稍微介绍下ConditionObject,写个小案例然后直入主题再来个图解就完活。

注意: 条件等待队列 (一定要和 等待队列 区分开来),这里我们画图解释一下:

几个注意的点:

  1. 在下文中: 只要提到等待队列,就是CLH队列,也就是存放 (获取锁失败后/或者被signal唤醒后从条件等待队列移到等待队列)的node队列,而一提到条件等待队列,就是在说(调用await后存放)Node的队列!,这俩队列一定要搞清楚,否则就很迷了。
  2. 条件等待队列可能存在多个,而CLH等待队列只能是一个。这一点我们要清楚。多个条件等待队列也是ReentrantLock实现细粒度唤醒的一个基本原因。
  3. AQS中的await和signale 只能是排他锁使用,共享锁绝对不会存在 等待/唤醒机制这么一说。
  4. 条件等待队列 中的线程,想要获取锁必然 需要通过signal方法 移动到等待队列中去才有机会
  5. 条件等待队列 和CLH一样也是FIFO 但是是单向链表结构这个要知道,另外signal唤醒的总是条件等待队列的头节点,await后插入的Node总是从条件等待队列的尾部进行插入。

1、ConditionObject有啥用以及小案例

对于ConditionObject,可能很多人没直接用过,但是如果你用过ReentrantLock,那么还是有一定概率使用到他的,尤其是在一些 生产/消费(或者说等待/唤醒) 场景下。ConditionObject他是AQS的一个内部类他实现了Condition接口,并且实现了其中的await(),signal(),signalAll()等方法,ConditionObject主要是为并发编程中的同步提供了等待/唤醒的实现方式,可以在不满足某个条件的时候挂起线程等待(使用await方法) 或者在满足某些条件时唤醒其他等待的线程(使用signal/signalAll方法)。就像使用synchroized时,使用的wait()和notify()/notifyAll()一样,只不过(基于ConditionObject实现的ReentrantLock)可以根据条件唤醒指定线程,而synchroized却不行他只能唤醒某一个或者全部唤醒,粒度没有 (基于ConditionObject实现的ReentrantLock)

在学习ConditionObject时,最好是研究过synchroized的实现以及监视器模型,那样你在学习过程中将两者对比一下,将会对他们(synchroized和ReentrantLock)的区别有更清楚的认识。如果对synchroized不了解或者不知道监视器模型的,去看看我的另一篇文章:万字长文分析synchroized ,相信你会有所收获。

由于jdk中基于ConditionObject实现的条件等待机制也就是ReentrantLock和读写锁,而ReentrantLock用的多一些所以我们以ReentrantLock为例,做一个生产/消费的小案例,来切身体会一下也方便源码分析时的切入和debug。

生产/消费 案例完整源码如下:

java 复制代码
/**
 * @Auther: Huangzhuangzhuang
 * @Date: 2023/10/20 07:02
 * @Description:
 */
@Slf4j
public class AwaitSignalDemo {

   private static volatile int shoeCount = 0;
   private static ThreadPoolExecutor producerThread = new ThreadPoolExecutor(1, 1, 1000 * 60, TimeUnit.MILLISECONDS, SemaphoreTest.asyncSenderThreadPoolQueue = new LinkedBlockingQueue<Runnable>(500), new ThreadFactory() {
      private final AtomicInteger threadIndex = new AtomicInteger(0);
      @Override
      public Thread newThread(Runnable r) {
         return new Thread(r, "生产线程_" + this.threadIndex.incrementAndGet());
      }
   });
   private static ThreadPoolExecutor consumerThread = new ThreadPoolExecutor(1, 1, 1000 * 60, TimeUnit.MILLISECONDS, SemaphoreTest.asyncSenderThreadPoolQueue = new LinkedBlockingQueue<Runnable>(500), new ThreadFactory() {
      private final AtomicInteger threadIndex = new AtomicInteger(0);
      @Override
      public Thread newThread(Runnable r) {
         return new Thread(r, "消费线程_" + this.threadIndex.incrementAndGet());
      }
   });

   public static void main(String[] args) {
      Lock lock = new ReentrantLock();
      Condition producerCondition = lock.newCondition();
      Condition consumerCondition = lock.newCondition();
      //不停生产鞋,攒够5双了就唤醒消费线程
      producerThread.execute(() -> {
         while (true) {
            lock.lock(); // 获取锁资源
            try {
               if (shoeCount > 5) { //如果生产够5双, 则阻塞等待生产线程,待消费线程消费完后再生产
                  System.out.println(Thread.currentThread().getName() + "_生产鞋完成" + (shoeCount - 1) + "双");
                  consumerCondition.signal();//唤醒消费鞋子的线程
                  producerCondition.await();//挂起生产鞋的线程
               } else {
                  shoeCount++;//生产鞋子
               }
            } catch (Exception e) {
               e.printStackTrace();
            } finally {
               lock.unlock();//释放锁资源
            }
         }
      });
      //不停消费鞋,把鞋消费完了就唤醒生产线程然他继续造
      consumerThread.execute(() -> {
         while (true) {
            lock.lock();//获取锁资源
            try {
               if (shoeCount == 0) {//如果消费完了
                  System.out.println(Thread.currentThread().getName() + "_鞋子全部消费完了");
                  System.out.println();
                  producerCondition.signal(); //消费完鞋子之后,唤醒生产鞋子的线程
                  consumerCondition.await(); //挂起消费鞋子的线程,等待生产完后唤醒当前挂起线程
               } else {
                  shoeCount--;//消费鞋子
               }
            } catch (Exception e) {
               e.printStackTrace();
            } finally {
               lock.unlock();//释放锁资源
            }
         }
      });
   }
}

代码逻辑不过多解释了,有注释也很简单没什么可说的。

分析ConditionObject的话,主要就是两个流程,一是等待(await),二是唤醒(signal/signalAll),我们分别来看下源码 然后和上篇的认真的并发基石:AQS源码分析与图解 一结合,你就对AQS的认识更全面了。

2、等待(await)机制源码分析

ReentrantLock的等待机制最终是依赖AQS的ConditionObject类的await方法实现的,所以我们直接来到AQS#ConditionObject的await方法一探究竟,源码如下:

java 复制代码
public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    //将当前线程加入到 条件等待的链表最后,并返回该节点(内部会创建 Node.CONDITION=-2 类型的 Node)
    Node node = addConditionWaiter();
    //释放当前线程获取的锁(通过操作 state 的值,一直减到state==0)释放了锁就会被阻塞挂起,
    //fullyRelease内部就是调用的我们在AQS独占锁释放时候的tryRelease方法
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    //判断 node节点是否在 AQS 等待队列中(注意该方法中如果node是head的话是返回false的,也就是会执行park逻辑)
    while (!isOnSyncQueue(node)) {
        //如果是head或者当前节点在队列则挂起当前线程
        LockSupport.park(this);
        //如果上边挂起线程后,紧接着又有其他线程中断/唤醒了当前线程(这种情况理论可能比较少但是并发情况下也不一定😄),那么则跳出循环,
        //下边(循环外的)acquireQueued将 node移至AQS等待队列,让其继续抢锁
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    //acquireQueued将 node移至AQS等待队列,让其再次抢锁
    //注意此处是 : 采用排他模式的资源竞争方法 acquireQueued
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        //清除取消的线程
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}


//将当前线程包装成 CONDITION 节点,排入该 Condition 对象内的(条件等待队列)的队尾
private Node addConditionWaiter() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node t = lastWaiter;
    //遍历 Condition 队列,踢出 Cancelled 节点
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    //将当前线程包装成 CONDITION 节点,排入该 Condition 对象内的条件等待队列的队尾
    Node node = new Node(Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}
//检测是否有中断
private int checkInterruptWhileWaiting(Node node) {
    return Thread.interrupted() ?
        (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
        0;
}

final boolean transferAfterCancelledWait(Node node) {
    //将node 状态由 CONDITION 设置为 0,如果设置成功,则说明当前线程抢占到了安排 node 进入 AQS 等待队列的权利,证明了 interrupt 操作先于 signal 操作
    if (node.compareAndSetWaitStatus(Node.CONDITION, 0)) {
        //加到等待队列
        enq(node);
        return true;
    }
    //如果 CAS 操作失败,说明其他线程调用 signal 先行处理了 node 节点。
    //当前线程没竞争到 node 节点的唤醒权,要在 node 节点进入 AQS 队列前一直自旋,同时要执行 yield 让出 CPU
    while (!isOnSyncQueue(node))
        Thread.yield();
    return false;
}
    

等待(即await)的源码考虑的很细,有些细节我们不做过多深挖,直接画个图演示下await主要做了什么:

3、唤醒(signal)机制源码分析与图解

java 复制代码
public final void signal() {
    //如果当前线程未持有资源state,则抛出异常
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

//唤醒
private void doSignal(Node first) {
    do {
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

//将node 节点从 条件等待队列转移到 等待队列中去
final boolean transferForSignal(Node node) {
    //尝试将节点状态由 CONDITION 改为 0
    if (!node.compareAndSetWaitStatus(Node.CONDITION, 0))
        return false;

    //end方法将 node 节点插入 AQS 等待队列 队尾,返回 node 节点的前驱节点
    Node p = enq(node);
    int ws = p.waitStatus;
    //如果当前node的前置节点状态为 CANCELLED(大于0只有取消一种),或者设置前置节点状态为 SIGNAL失败,则将 node 节点持有的线程唤醒
    if (ws > 0 || !p.compareAndSetWaitStatus(ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

唤醒机制(signal)的图解:

4、总结

本篇是对上一篇认真的并发基石:AQS源码分析与图解的补充,如果认真看了的话,我相信至少你可以把AQS内部的主逻辑和一些不太复杂的细节搞清楚了,另外对常用的并发工具和ReentrantLock应该也清楚了些。其实就是玩的state和俩队列(一个等待队列,一个条件等待队列)。 到这里AQS相关知识就结束了。下篇文章我们将着手其他的技术分析(是啥我还没想好)。我是蝎子莱莱,在学习的路上 ,我一直在!!!

相关推荐
等一场春雨29 分钟前
Java设计模式 八 适配器模式 (Adapter Pattern)
java·设计模式·适配器模式
一弓虽1 小时前
java基础学习——jdbc基础知识详细介绍
java·学习·jdbc·连接池
王磊鑫1 小时前
Java入门笔记(1)
java·开发语言·笔记
马剑威(威哥爱编程)1 小时前
2025春招 SpringCloud 面试题汇总
后端·spring·spring cloud
硬件人某某某1 小时前
Java基于SSM框架的社区团购系统小程序设计与实现(附源码,文档,部署)
java·开发语言·社区团购小程序·团购小程序·java社区团购小程序
程序员徐师兄1 小时前
Java 基于 SpringBoot 的校园外卖点餐平台微信小程序(附源码,部署,文档)
java·spring boot·微信小程序·校园外卖点餐·外卖点餐小程序·校园外卖点餐小程序
chengpei1472 小时前
chrome游览器JSON Formatter插件无效问题排查,FastJsonHttpMessageConverter导致Content-Type返回不正确
java·前端·chrome·spring boot·json
Quantum&Coder2 小时前
Objective-C语言的计算机基础
开发语言·后端·golang
五味香2 小时前
Java学习,List 元素替换
android·java·开发语言·python·学习·golang·kotlin
Joeysoda2 小时前
Java数据结构 (从0构建链表(LinkedList))
java·linux·开发语言·数据结构·windows·链表·1024程序员节