深入浅出AQS之条件队列

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

原文地址:

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

一、使用场景介绍

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

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

上面的代码片段截取自LinkedBlockingQueue,是Java常用的阻塞队列之一。
从上面的代码可以看出,条件队列是建立在锁基础上的,而且必须是独占锁(原因后面会通过源码分析)。

二、执行过程概述

等待条件的过程:

在操作条件队列之前首先需要成功获取独占锁,不然直接在获取独占锁的时候已经被挂起了。

成功获取独占锁以后,如果当前条件还不满足,则在当前锁的条件队列上挂起,与此同时释放掉当前获取的锁资源。这里可以考虑一下如果不释放锁资源会发生什么?

如果被唤醒,则检查是否可以获取独占锁,否则继续挂起。

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

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

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

二、源码深入分析

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

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

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

//注:1.与同步队列不同,条件队列头尾指针是firstWaiter跟lastWaiter //注:2.条件队列是在获取锁之后,也就是临界区进行操作,因此很多地方不用考虑并发 private Node addConditionWaiter() { Node t = lastWaiter; //如果最后一个节点被取消,则删除队列中被取消的节点 //至于为啥是最后一个节点后面会分析 if (t != null && t.waitStatus != Node.CONDITION) { //删除所有被取消的节点 unlinkCancelledWaiters(); t = lastWaiter; } //创建一个类型为CONDITION的节点并加入队列,由于在临界区,所以这里不用并发控制 Node node = new Node(Thread.currentThread(), Node.CONDITION); if (t == null) firstWaiter = node; else t.nextWaiter = node; lastWaiter = node; return node; } //删除取消节点的逻辑虽然长,但比较简单,就不单独说了,就是链表删除 private void unlinkCancelledWaiters() { Node t = firstWaiter; Node trail = null; while (t != null) { Node next = t.nextWaiter; if (t.waitStatus != Node.CONDITION) { t.nextWaiter = null; if (trail == null) firstWaiter = next; else trail.nextWaiter = next; if (next == null) lastWaiter = trail; } else trail = t; t = next; } }

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zgyjgp.html