AQS(AbstractQueuedSynchronizer)源码深度解析(1)—AQS的总体设计与架构

152 阅读2分钟

「这是我参与2022首次更文挑战的第25天,活动详情查看:2022首次更文挑战」。

详细介绍了AQS(AbstractQueuedSynchronizer)的设计思想,以及总体设计结构。

AQS相关文章:

AQS(AbstractQueuedSynchronizer)源码深度解析(1)—AQS的设计与总体结构

AQS(AbstractQueuedSynchronizer)源码深度解析(2)—Lock接口以及自定义锁的实现

AQS(AbstractQueuedSynchronizer)源码深度解析(3)—同步队列以及独占式获取锁、释放锁的原理【一万字】

AQS(AbstractQueuedSynchronizer)源码深度解析(4)—共享式获取锁、释放锁的原理【一万字】

AQS(AbstractQueuedSynchronizer)源码深度解析(5)—条件队列的等待、通知的实现以及AQS的总结【一万字】

1 从AQS学起

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements Serializable

AbstractQueuedSynchronizer,来自于JDK1.5,位于JUC包,由并发编程大师Doug Lea编写,字面翻译就是“抽象队列同步器”,简称为AQS。AQS作为一个抽象类,是构建JUC包中的锁(比如ReentrantLock)或者其他同步组件(比如CountDownLatch)的底层基础框架。

在每一个同步组件的实现类中,都具有AQS的实现类作为内部类,被用来实现该锁的内存语义,并且他们之间是强关联关系,从对象的关系上来说锁或同步器与AQS是:“聚合关系”。

也可以这样理解二者之间的关系:

  1. 锁(比如ReentrantLock)是面向使用者(大部分“用轮子”程序员)的,它定义了使用者与锁交互的外部接口,比如获得锁、释放锁的接口,这样就隐藏了实现细节,方便学习使用;
  2. AQS则是面向的是锁的实现者(少部分“造轮子”的程序员,比如Doug Lea,这个比喻并不恰当,因为Doug Lea是整个JUC包的编写者,包括AQS也是他写的),因为AQS简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、线程的等待与唤醒等更加底层的操作,我们如果想要自己写一个“锁”—造轮子,那么就可以直接利用AQS框架实现,而不需要去关心上面那些更加底层的东西。这样的话似乎也不算真正的从零开始造轮子,因为我们用的AQS这个制造工具也是别人(Doug Lea)制作的::>_<::。

锁和AQS很好地隔离了使用者和实现者所需关注的领域。如果我们只是想单纯的使用某个锁,那个直接看锁的API就行了,而如果我们想要看懂某个锁的实现,那么我们就需要看锁的源码,在这之中我们又可能会遇到AQS框架的某个方法的调用;如果我们想要走得更远,那么此时又会进入AQS的源码,那么我们必须去了解AQS这个同步框架的设计与实现!

如果我们想要真正学搞懂JUC的同步组件,那么,先从AQS学起吧!

2 AQS的设计

AbstractQueuedSynchronizer被设计为一个抽象类,它使用了一个volatile int类型的成员变量state来表示同步状态(或者说资源),通过一个内置的FIFO双向同步队列来完成资源获取线程的排队等待工作,通过一个或者多个ConditionObject条件队列来实现线程之间的通信(等待和唤醒)。

通常AQS的子类通过继承AQS并实现它的抽象方法来管理同步状态,但是AQS的实现类通常也不会作为开发人员直接使用的同步组件,而是作为同步组件的静态内部类(也就是聚合关系),因为AQS自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供同步组件使用,AQS既可以支持独占式地访问同步状态(比如ReentrantLock),也可以支持共享式地访问同步状态(比如CountDownLatch),这样就可以方便实现不同类型的同步组件。

AQS的方法设计是基于模板方法模式的,也就是说实现者需要写一个实现类继承AQS并按照需要重写可重写的方法,随后将AQS的实现类组合在真正的同步组件的实现中,并最终在同步组件的方法中调用AQS提供的模板方法来实现同步,而这些模板方法内部实际上被设计成会调用使用者重写的可重写的方法。

因此,AQS的方法可以分为三大类:固定方法、可重写的方法、模版方法。

2.1 固定方法

重写AQS指定的方法时,需要使用AQS提供的如下3个方法来访问或修改同步状态,不同的锁实现都可以直接调用这三个方法:

  1. getState():获取当前最新同步状态。
  2. setState(int newState):设置当前最新同步状态。
  3. compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性。

它们的源码很简单:

/**
 * int类型同步状态变量,或者代表共享资源,被volatile修饰,具有volatile的读、写的内存语义
 */
private volatile int state;

/**
 * @return 返回同步状态的当前值。此操作具有volatile读的内存语义,因此每次获取的都是最新值
 */
protected final int getState() {
    return state;
}

/**
 * @param newState 设置同步状态的最新值。此操作具有volatile写的内存语义,因此每次写数据都是写回主存并导致其它缓存实效
 */
protected final void setState(int newState) {
    state = newState;
}

/**
 * 如果当前状态值等于预期值,则以原子方式将同步状态设置为给定的更新值。
 * 此操作具有volatile读取和写入的内存语义。
 *
 * @param expect 预期值
 * @param update 写入值
 * @return 如果更新成功返回true,失败则返回false
 */
protected final boolean compareAndSetState(int expect, int update) {
    //内部调用unsafe的方法,该方法是一个CAS方法
    //这个unsafe类,实际上是比AQS更加底层的底层框架,或者可以认为是AQS框架的基石。
    //CAS操作在Java中的最底层的实现就是Unsafe类提供的,它是作为Java语言与Hospot源码(C++)以及底层操作系统沟通的桥梁
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

这三个方法getState()、setState()、compareAndSetState()都是final方法,是AQS提供的通用设置同步状态的方法,能保证线程安全,我们直接调用即可。

2.2 可重写的方法

可重写的方法在AQS中一般都没有提供实现,如果子类不重写就直接调用还会抛出异常,这些方法一般是对同步状态的单次尝试获取、释放(即加锁、解锁),并没有后续失败处理的方法!实现者一般根据需要重写对应的方法!

AQS可重写的方法如下所示:

/**
 * 独占式获取锁,该方法需要查询当前状态并判断锁是否符合预期,然后再进行CAS设置锁。返回true则成功,否则失败。
 *
 * @param arg 参数,在实现的时候可以传递自己想要的数据
 * @return 返回true则成功,否则失败。
 */
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

/**
 * 独占式释放锁,等待获取锁的线程将有机会获取锁。返回true则成功,否则失败。
 *
 * @param arg 参数,在实现的时候可以传递自己想要的数据
 * @return 返回true则成功,否则失败。
 */
protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}

/**
 * 共享式获取锁,返回大于等于0的值表示获取成功,否则失败。
 *
 * @param arg 参数,在实现的时候可以传递自己想要的数据
 * @return 返回大于等于0的值表示获取成功,否则失败。
 * 如果返回值小于0,表示当前线程共享锁失败
 * 如果返回值大于0,表示当前线程共享锁成功,并且接下来其他线程尝试获取共享锁的行为很可能成功
 * 如果返回值等于0,表示当前线程共享锁成功,但是接下来其他线程尝试获取共享锁的行为会失败(实际上也有可能成功,在后面的源码部分会将)
 */
protected int tryAcquireShared(int arg) {
    throw new UnsupportedOperationException();
}

/**
 * 共享式释放锁。返回true成功,否则失败。
 *
 * @param arg 参数,在实现的时候可以传递自己想要的数据
 * @return 返回true成功,否则失败。
 */
protected boolean tryReleaseShared(int arg) {
    throw new UnsupportedOperationException();
}

/**
 * 当前AQS是否在独占模式下被线程占用,一般表示是否被前当线程独占;如果同步是以独占方式进行的,则返回true;其他情况则返回 false
 *
 * @return 如果同步是以独占方式进行的,则返回true;其他情况则返回 false
 */
protected boolean isHeldExclusively() {
    throw new UnsupportedOperationException();
}

实际上,上面的acquire和release系列方法并不是一定代表着锁的获取和释放,它的具体含义要看同步组件的具体实现,因为AQS不仅仅可被用来实现锁,还可以被用来实现其他的同步组件,因此这得获取和释放的应该被叫做资源。

2.3 模版方法

在实现同步组件的时候,对于内部的AQS的实现类按照需要重写可重写的方法,但是同步组件开放出来的方法中还是直接调用的AQS提供的模板方法。

这些模板方法同样是final的,这些模版方法包含了对上面的可重写方法的调用,以及后续处理,比如失败处理!

AQS的模板方法基本上分为3类:

  1. 独占式获取与释放同步状态
  2. 共享式获取与释放同步状态
  3. 查询同步队列中的等待线程情况

独占方式

  1. acquire(int arg):独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待。该方法不会响应中断。该方法内部调用了可重写的tryAcquire方法。

  2. acquireInterruptibly(int arg):与acquire方法相同,但是该方法响应中断,当前线程未获取到同步状态而进入同步队列中,如果当前被中断,则该方法会抛出InterruptedException并返回。

  3. tryAcquireNanos(int arg,long nanos):在acquireInterruptibly方法基础上增加了超时限制,如果当前线程在超时时间内没有获取到同步状态,那么将会返回false,获取到了返回true。

  4. release(int arg):独占式的释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个结点包含的线程唤醒。该方法内部调用了可重写的tryRelease方法。

共享方式:

  1. acquireShared(int arg):共享式获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待。与独占式的不同是同一时刻可以有多个线程获取到同步状态。该方法不会响应中断。该方法内部调用了可重写的tryAcquireShared方法。

  2. acquireSharedInterruptibly(int arg):与acquireShared (int arg) 相同,但是该方法响应中断,当前线程未获取到同步状态而进入同步队列中,如果当前被中断,则该方法会抛出InterruptedException并返回。

  3. tryAcquireSharedNanos(int arg,long nanos):在acquireSharedInterruptibly方法基础上增加了超时限制,如果当前线程在超时时间内没有获取到同步状态,那么将会返回false,获取到了返回true。

  4. releaseShared(int arg):共享式释放同步状态,该方法会在释放同步状态之后,尝试唤醒同步队列中的后继节点中的线程。该方法内部调用了可重写的tryReleaseShared方法。

获取线程等待情况:

  1. getQueuedThreads():获取等待在同步队列上的线程集合。

3 总体结构总结

AQS中文名为队列同步器,可以猜测,它的内部具有一个队列,实际上也确实如此。

AQS内部使用一个一个FIFO的双端队列,被称为同步队列,来完成同步状态的管理,当前线程获取同步状态失败(获取锁失败)的时候,AQS会将当前线程及其等待状态信息构造成一个结点Node并将其加入同步队列中,同时阻塞当前线程,当同步状态由持有线程释放的时候,会将同步队列中的首结点中的线程唤醒使其再次尝试获取同步状态。

同步队列中的结点Node是AQS中的一个内部类,用来保存获取同步状态失败的线程的线程引用、等待状态以及前驱结点和后继结点。AQS持有同步队列的两个引用,一个指向头结点head,而另一个指向尾结点tail。

在AQS中还维持了一个volatile int类型的字段state,用来描述同步状态,可以通过getState、setState、compareAndSetState函数修改其值。

对于不同的同步组件的实现来说,state可以有不同的含义。对于ReentrantLock 的实现来说,state可以用来表示当前线程获取锁的可重入次数;对于读写锁ReentrantReadWriteLock 来说,state 的高16位表示读状态,也就是获取该读锁的次数,低16位表示获取到写锁的线程的可重入次数;对于Semaphore来说,state用来表示当前可用信号的个数:对于CountDownlatch 来说,state 用来表示计数器当前的值。

AQS内部还有一个ConditionObject内部类,用来结合锁实现更加灵活的线程同步。ConditionObject 可以直接访问AQS 对象内部的变量,比如state 状态值和AQS 同步队列。ConditionObject 又被称为条件变量,每个条件变量实例的内部又维护(对应)了一个条件队列(单向链表,又称等待队列),其用来存放调用Condition的await方法后被阻塞的线程,这个条件队列的头、尾元素分别由firstWaiter 和lastWaiter持有。

上面的介绍能看出来,AQS中包含两个队列的实现,一个同步队列,用于存放获取不到锁的线程,另一个是条件队列,用于存放调用了await方法的线程,但是在两个队列中等待的线程都是WAITING状态,因为Lock锁底层都是调用的LockSupport.park方法来实现阻塞等待的。一个AQS实例拥有一个同步队列,可拥有多个条件队列。

本章节先介绍AQS相关的方法以及整体结构,后面的章节会讲解AQS的源码!

如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!