AQS(AbstractQueuedSynchronizer)源码深度解析—AQS的设计与总体结构
Posted 刘Java
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了AQS(AbstractQueuedSynchronizer)源码深度解析—AQS的设计与总体结构相关的知识,希望对你有一定的参考价值。
详细介绍了AQS的设计思想,以及总体设计结构。
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是:“聚合关系”。
也可以这样理解二者之间的关系:
- 锁(比如ReentrantLock)是面向使用者(大部分“用轮子”程序员)的,它定义了使用者与锁交互的外部接口,比如获得锁、释放锁的接口,这样就隐藏了实现细节,方便学习使用;
- 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双向队列来完成资源获取线程的排队等待工作。通常AQS的子类通过继承AQS并实现它的抽象方法来管理同步状态。
AQS的子类常常作为同步组件的静态内部类(也就是聚合关系),AQS自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供同步组件使用,AQS既可以支持独占式地访问同步状态(比如ReentrantLock),也可以支持共享式地访问同步状态(比如CountDownLatch),这样就可以方便实现不同类型的同步组件。
AQS的方法设计是基于模板方法模式的,也就是说实现者需要继承AQS并按照需要重写指定的方法,随后将AQS的实现类组合在同步组件的实现中,并最终调用AQS提供的模板方法来实现同步,而这些模板方法内部实际上被设计成会调用使用者重写的方法。
因此,AQS的方法可以分为三大类:固定方法、可重写的方法、模版方法。
2.1 固定方法
重写AQS指定的方法时,需要使用AQS提供的如下3个方法来访问或修改同步状态,不同的锁实现都可以直接调用这三个方法:
- getState():获取当前最新同步状态。
- setState(int newState):设置当前最新同步状态。
- 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();
}
2.3 模版方法
在实现同步组件的时候,按照需要重写可重写的方法,但是直接调用的还是AQS提供的模板方法,模版方法再被Lock接口的方法包装。
这些模板方法同样是final的。可以猜测出这些模版方法包含了对上面的可重写方法的后续处理(比如失败处理)!
AQS的模板方法基本上分为3类:
- 独占式获取与释放同步状态
- 共享式获取与释放同步状态
- 查询同步队列中的等待线程情况
独占方式:
- acquire(int arg): 独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待,该方法将会调用重写的tryAcquire(int arg) 方法。该方法不会响应中断。
- acquireInterruptibly(int arg): 与acquire(int arg) 相同,但是该方法响应中断,当前线程未获取到同步状态而进入同步队列中,如果当前被中断,则该方法会抛出InterruptedException并返回。
- tryAcquireNanos(int arg,long nanos): 在acquireInterruptibly基础上增加了超时限制,如果当前线程在超时时间内没有获取到同步状态,那么将会返回false,获取到了返回true。
- release(int arg) : 独占式的释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个结点包含的线程唤醒。
共享方式:
- acquireShared(int arg): 共享式获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待。与独占式的不同是同一时刻可以有多个线程获取到同步状态。该方法不会响应中断。
- acquireSharedInterruptibly(int arg) : 与acquireShared(int arg) 相同,但是该方法响应中断,当前线程未获取到同步状态而进入同步队列中,如果当前被中断,则该方法会抛出InterruptedException并返回。
- tryAcquireSharedNanos(int arg,long nanos): 在acquireSharedInterruptibly基础上增加了超时限制,如果当前线程在超时时间内没有获取到同步状态,那么将会返回false,获取到了返回true
- releaseShared(int arg): 共享式释放同步状态
获取线程等待情况:
- 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的核心!
如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!
以上是关于AQS(AbstractQueuedSynchronizer)源码深度解析—AQS的设计与总体结构的主要内容,如果未能解决你的问题,请参考以下文章
AQS(AbstractQueuedSynchronizer)源码深度解析—AQS的设计与总体结构