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


highlight: androidstudio

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

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

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

几个注意的点: 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。 image.png

生产/消费 案例完整源码如下: ```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 (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 (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主要做了什么: image.png

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)的图解: image.png

4、总结

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值