Java并发与锁设计实现详述(5)- AQS(AbstractQueuedSynchronizer)的实现

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/andamajing/article/details/79748073

在这篇文章中我们来看一个非常非常重要的东西,那就是AbstractQueuedSynchronizer,简称AQS,又叫队列同步器。

为什么说它重要呢?因为它是Java并发包Java.util.concurrent的核心所在,讲到Java并发,如果不知道AQS那你说你懂并发可能就有点过了^_^

Java.util.concurrent并发包是谁写的呢?是下面这个大神Doug Lea,先来看一眼,虽然这辈子也见不到哈哈


是不是看着还蛮和蔼的,他提出了JSR166规范,该规范的核心就是AbstractQueuedSynchronizer同步器框架(AQS),这个框架为Java中构造同步器提供一种通用的机制。

在JDK1.7中,我们可以查看源码,大概看下都有哪些类,如下所示:


其中,其中AbstractOwnableSynchronizer是其父类,而AbstractQueuedLongSynchronizer是其32位状态的升级版64位的实现,适用于多级屏障(CyclicBarrier)。

那么AQS到底是什么,做什么用的呢?我截取了源码中关于这个类的一个基本说明,如下:

/**
 * Provides a framework for implementing blocking locks and related
 * synchronizers (semaphores, events, etc) that rely on
 * first-in-first-out (FIFO) wait queues.  This class is designed to
 * be a useful basis for most kinds of synchronizers that rely on a
 * single atomic <tt>int</tt> value to represent state. Subclasses
 * must define the protected methods that change this state, and which
 * define what that state means in terms of this object being acquired
 * or released.  Given these, the other methods in this class carry
 * out all queuing and blocking mechanics. Subclasses can maintain
 * other state fields, but only the atomically updated <tt>int</tt>
 * value manipulated using methods {@link #getState}, {@link
 * #setState} and {@link #compareAndSetState} is tracked with respect
 * to synchronization.
 *

翻译如下:该类提供了一个用于实现阻塞锁和依赖先入先出等待队列的相关同步器(如信号量、事件等)的一个框架。设计出该类是为了作为许多其他使用一个原子int型数值来代表同步状态的同步器的一个基石。继承该类的子类必须重新定义一些改变同步状态的protected方法,包括同步状态的获取、获取与释放。鉴于此,在AQS中的其他方法实现了所有的队列和阻塞机制。当然,子类还可以维护自己的状态字段,但是需要原子的更新状态值。(个人英文水平有限,如有不对,敬请谅解!)

基于上面的说明,我们可以知道AQS框架是基于模板方式设计的,提供基本的访问限制控制,而由子类自行定义状态的变更规则,从而实现不同的同步器。


在AQS框架中,共定义了两种资源共享模式。

Exclusive:独占模式,同时只有一个线程能执行,如ReentrantLock

Share:共享模式,多个线程可同时执行,如Semaphore/CountDownLatch。

一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。

自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:

  • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
  • tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
  • tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
  • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):共享方式。尝试释放资源,成功则返回true,失败则返回false。 
AQS为了实现上述操作,需要下面三个基本组件的相互协作:
  • 同步状态的原子性管理;
  • 线程的阻塞与解除阻塞;
  • 队列的管理;
(一)同步状态的原子性管理

在AQS中,同步状态是用一个volatile关键字修饰int型变量来标识的(至于这里为什么使用32位的Int型变量,而不是64位的Long变量,这里不做说明,有兴趣的可以自己考虑一下),通过CAS和volatile来保证同步状态更新的原子性和可见性。

关于volatile关键字的说明可以参见我的另一篇博客<Java并发与锁设计实现详述(9)- 关键字volatile底层原理>。


而关于CAS,我们在<Java并发与锁设计实现详述(6)- 聊一聊Unsafe>这篇文章已经说到了,在JAVA并发框架里,并发包的核心是AQS,而AQS的核心是UnSafe类,UnSafe类中提供了用于同步的native方法CAS,用于各种类型变量的原子性操作。


(二)线程的阻塞与解除阻塞

在AQS中是通过LockSupport.park() 和 LockSupport.unpark() 来实现线程的阻塞和唤醒(底层调用Unsafe的native park和unpark实现),同时支持超时时间。如下所示:


(三)队列管理

在线程尝试获取同步锁时,如果能够直接获取成功,那么就直接返回,否则当前线程将会被加入到等待队列,并在队列中自旋,知道获取到同步锁或者被中断退出,在自旋过程中并不响应中断。


下面结合源码来看看获取独占锁是怎么实现的。

获取独占锁的入口:

从代码逻辑上看,可以理解为:(1)先尝试获取独占锁;(2)如果没有获取到独占锁,则将当前线程加入到等待队列,并且进行自旋等待获取独占锁;(3)如果在自旋过程中遇到线程中断(在自旋过程中是不进行中断相应的),还要进行中断处理;

下面来看看每一步具体都是怎么做的。

首先是tryAcquire(arg),这个方法在AQS中是没有实现的,目的是为了让继承类自己去控制是否可以获取锁,需要继承类自己去实现。接下来就是addWaiter(Node.EXCLUSIVE),这个方法实现将当前线程加入到等待队列中。


在上面首先创建了一个Node节点,然后判断是否有尾节点,如果为节点不为空,那么会尝试从尾部通过compareAndSetTail快速向尾部插入节点。如果尝试插入失败了,那么再走正常的流程向队列中插入一个节点,即再次调用enq(node)插入节点。


我们看到这里用了一个for(;;)死循环进行自旋,只有成功插入了节点才会从循环中退出。每次都会尝试使用CAS操作来更新尾节点,如果更新成功了就认为插入队列成功了,否则认为失败会进行下一次尝试。

将当前未获取到同步锁的线程封装到Node节点里并添加到等待队列后,后续又做些什么呢?返回到上面acquire方法,可以知道紧接着进行了一次acquireQueued方法,这个方法是干嘛的呢?看看源码:


这里有两个局部变量,failed和interrupted,failed用于记录获取同步锁是否成功,而interrupted用于记录在park过程中是否有中断从而记录中断状态。

从整体上看是一个for(;;)死循环,这这个循环里只有一种情况可以跳出循环,那就是在等待队列中当前节点前面没有等待同步锁的其他Node节点时且当前节点成功获取到同步锁才会跳出死循环。如果不是上面的这种情况,那么会不断尝试等待。

这块代码就是标识当前节点前面已经没有等待同步锁的Node节点了,并且通过tryAcquire(arg)成功获取到同步锁后成功返回。

如果没有的话就自旋不断进行下面的处理:

我们来看看这两个方法都是干什么的。一个是shouldParkAfterFailedAcquire(p,node),一个是parkAndCheckInterrupt()。

前者从方法命名上看应该是指在获取同步锁失败之后是否需要park,而后者应该是进行park操作并检查是否遭遇到中断。

先看下前者源码:


从这个方法的说明第一句可以看出,这个方法是用来在获取同步锁失败之后检查和更新节点状态值的。那么节点的状态值都有哪些呢?在源码中找到了一下的一块Node节点状态定义:

waitstatus分为4个状态:CANCELED(1)、SIGNAL(-1)、CONDITION(-2)、PROPAGATE(-3)。

回到前面shouldParkAfterFailedAcquire(p,node)的逻辑,根据前节p的状态值进行相应的处理,如果前驱节点是SIGNAL状态,那么返回true,表示当前节点一定要park。如果前驱节点取消了,即ws>0,则从前驱节点开始往前寻找知道找到一个ws<=0的节点并返回false,标识此次操作不需要park,需要进行下一次的自旋重试。如果不是这两种情况,那么需要将这个前驱节点的状态更新为SIGNAL状态,并返回false,标识此次自旋过程不需要park。

接下来看看另一个方法,在需要park情况下,需要继续调用parkAndCheckInterrupt()方法:


这个方法通过借助LockSupport工具类实现park功能,并且返回线程的中断状态。

讲完了这两个方法,我们回到之前的acquireQueued方法,在方法的尾部我们看到了,如果失败了,会进行cancelAcquire操作,这个操作又是干嘛的呢?来看看源码:


上述标号为1的代码,对当前Node节点中的线程和ws状态进行了更新,标号为2的代码对当前节点前的已取消线程进行了筛选,标号为3的代码用于快速判断当前节点是否是尾节点,如果是就直接删除该节点。标号为4的代码用于对处在非尾节点的当前Node节点进行处理。

最后回到acquire(arg)方法,如果在acquireQueued()过程中遇到了中断,那么会调用selfInterrupt方法,如下:

这里会返回当前线程的中断状态。

至此,一次获取独占锁的过程便全部说完了,有兴趣的朋友可以自己对照着源码看看。和获取同步锁相对应的应该就是释放同步锁了,我们接下来看看release(arg)具体逻辑是怎样的。


首先是调用tryRelease尝试释放同步锁,如果失败了直接返回false,如果成功了,则进行相应的其他处理。tryRelease方法和tryAcquire方法一样,是由继承类自己根据需要的特性来自己进行实现的。

我们接下来unparkSuccessor(h)方法,


这个方法的目的是唤醒当前节点的后继节点,如果存在的话。在上面的代码中,通过从等待队列尾部逆向往前找到当前节点后的真正后继节点(将中间的CANCEL状态节点去掉)。找到这个节点后使用LockSupport.unpark方法唤醒当前节点的后继节点。

至此,对于AQS的基本框架简单的说明了下,并且结合源码对独占锁的获取和释放进行了详细的解释,当然在AQS中不仅包含这些,还包含对共享锁的获取和释放,这部分因为时间和篇幅问题,这里就不在说明了,感兴趣的朋友可以自己结合源码去看,谢谢。

感谢大家的阅读,如果有对Java编程、中间件、数据库、及各种开源框架感兴趣,欢迎关注我的博客和头条号(源码帝国),博客和头条号后期将定期提供一些相关技术文章供大家一起讨论学习,谢谢。

如果觉得文章对您有帮助,欢迎给我打赏,一毛不嫌少,一百不嫌多,^_^谢谢。


展开阅读全文

没有更多推荐了,返回首页