并发——抽象队列同步器AQS的实现原理
1|0一、前言
这段时间在研究Java
并发相关的内容,一段时间下来算是小有收获了。ReentrantLock
是Java
并发中的重要部分,所以也是我的首要研究对象,在学习它的过程中,我发现它是基于抽象队列同步器AQS实现的,所以我花了点时间学习了一下AQS
的实现原理。这篇博客就来说一说AQS
的作用,以及它是如何实现的。
2|0二、正文
2|12.1 什么是AQS
AQS
全称抽象队列同步器(AbstractQuenedSynchronizer),它是一个可以用来实现线程同步的基础框架。当然,它不是我们理解的Spring
这种框架,它是一个类,类名就是AbstractQuenedSynchronizer
,如果我们想要实现一个能够完成线程同步的锁或者类似的同步组件,就可以在使用AQS
来实现,因为它封装了线程同步的方式,我们在自己的类中使用它,就可以很方便的实现一个我们自己的锁。
2|22.2 如何使用AQS
AQS
封装了很多方法,如获取独占锁,释放独占锁,获取共享锁,释放共享锁......我们可以通过在自己的实现的同步组件中调用AQS
的这些方法来实现一个线程同步的功能。但是,根据AQS
的名称也能够想到,我们不能直接创建AQS
的对象,调用这些方法,因为AQS
是一个抽象类,我们需要继承AQS
,创建它的子类对象来使用它。在实际使用中,一般是在我们自己的类中,以内部类的方式继承AQS
,然后在内部创建一个对象,在这个类内部使用,比如ReentrantLock
中就是定义了一个抽象内部类Sync
,继承AQS
,然后定义了一个NonfairSync
类,继承Sync
,NonfairSync
是一个非公平锁;同时又定义了一个FairSync
类继承Sync
,FairSync
是一个公平锁。
公平锁:多个线程按照申请锁的顺序去获得锁,后申请锁的线程需要排队,等它之前的线程获得锁并释放后,它才能获得锁;
非公平锁:线程获得锁的顺序于申请锁的顺序无关,申请锁的线程可以直接尝试获得锁,谁抢到就是谁的;
我们继承了AQS
,就可以直接调用它的方法了吗?当然不是。Java
中提供的抽象组件,都是帮我们写好了通用的部分,但是一些具体的部分,还需要我们自己实现。举个比较简单的例子,Java
中对自定义类型数组的排序,可以直接调用工具类的sort
方法,sort
方法已经实现了排序的算法,但是其中的比较过程是抽象的,需要我们自己实现,所以我们一般需要提供一个比较器(Comparator),或者让自定义类实现Comparable
接口。这就是模板方法设计模式。
模板方法:在一个方法中实现了一个算法的流程,但是其中的一些步骤是抽象的,需要在子类中实现,或者具体使用时实现。模板方法可以提高算法的复用性,提供了算法的弹性,对于不同的需求,可以通用同一份代码。
而AQS
的实现就是封装了一系列的模板方法,包括获取锁、释放锁等,这些都是模板方法。这些方法中调用的一些方法并没有具体实现,需要使用者根据自己的需求,在子类中进行实现。下面我们就来看看AQS
中的这些方法。
2|32.3 AQS中的方法
AQS底层维护一个int类型的变量state来表示当前的同步状态,根据当前state的值,来判断当前释放处于锁定状态,或者是其他状态。而state
的每一个值具体是什么含义,是由我们自己实现的。我们继承AQS
时,根据自己的需求,实现一些方法,其中就是通过修改state
的值来维持同步状态。而关于state
,主要有以下三个方法:
- **int getState() **:获取当前同步状态
state
的值; - **void setState(int newState) **:设置当前同步状态
state
的值; - **boolean compareAndSetState(int expect, int update) **:使用
CAS
设置当前同步状态的值,方法能够保证设置同步状态时的原子性;参数expect
为state
的预期旧值,而update
是需要修改的新值,若设置成功,方法返回true
,否则false
;
CAS是一种乐观锁,若不了解,可以看看这篇博客:并发——详细介绍CAS机制
接下来我们再看一看在继承AQS
时,我们可以重写的方法:
以上这些方法将会在AQS
的模板方法中被调用,我们根据自己的需求,重写上述方法,控制同步状态state
的值,即可控制线程同步的方式。下面再来看看AQS
提供的模板方法:
AQS
提供的模板方法主要分为三类:
- 独占式地获取和释放锁;
- 共享式地获取和释放锁;
- 查询
AQS
的同步队列中正在等待的线程情况;
下面我们就来具体说一说AQS
是如何实现线程同步的。
2|42.4 AQS如何实现线程同步
前面提过,AQS
通过一个int
类型的变量state
来记录当前的同步状态,也可以理解为锁的状态,根据state
的值的不同,可以判断当前锁是否已经被获取。就拿独占锁来说,若我们要实现的是一个独占锁,则锁被获取后,其他线程将无法获取锁,需要进入阻塞状态,等待锁被释放。而线程获取锁就是通过修改state
的值来实现的,一个线程修改state
成功,则表示它成功获得了锁;若失败,则表示已经有其他线程获得了锁,则它需要进入阻塞状态。下面我们就来聊一聊AQS
如何实现维持多个线程等待的。
首先说明结论:AQS通过一个同步队列来维护当前获取锁失败,进入阻塞状态的线程。这个同步队列是一个双向链表,获取锁失败的线程会被封装成一个链表节点,加入链表的尾部排队,而AQS
保存了链表的头节点的引用head
以及链表的尾节点引用tail
。这个同步队列如下所示:
在这个同步队列中,每个节点对应一个线程,每个节点都有一个next
指针指向它的下一个节点,以及一个prev
指针指向它的上一个节点。队列中的头节点head
就是当前已经获取了锁,正在执行的线程对应的节点;而之后的这些节点,则对应着获取锁失败,正在排队的线程(当然,直接就获取锁成功的线程, 不会加入到队列中,而是直接执行)。
当一个线程获取锁失败,它会被封装成一个Node
,加入同步队列的尾部排队,同时线程会进入阻塞状态。也就是说,在同步队列中,除了头节点对应的线程是运行状态,其余的线程都是等待睡眠状态。而当头节点对应的线程释放锁时,它会唤醒它的下一个节点(也就是上图中的第二个节点),被唤醒的节点对应的线程开始尝试获取锁,若获取成功,它就会将自己置为head
,然后将原来的head
移出队列。接下来我们就通过源码,具体分析一下AQS
的实现过程。
2|52.5 独占锁的获取与释放过程
(1)获取锁的实现
AQS
的锁功能齐全,它既可以用来实现独占锁,也可以用来实现共享锁。
独占锁:也叫排他锁,即锁只能由一个线程获取,若一个线程获取了锁,则其他想要获取锁的线程只能等待,直到锁被释放。比如说写锁,对于写操作,每次只能由一个线程进行,若多个线程同时进行写操作,将很可能出现线程安全问题;
共享锁:锁可以由多个线程同时获取,锁被获取一次,则锁的计数器+1。比较典型的就是读锁,读操作并不会产生副作用,所以可以允许多个线程同时对数据进行读操作,而不会有线程安全问题,当然,前提是这个过程中没有线程在进行写操作;
我们首先分析一下独占锁。在AQS
中,通过方法acquire
来获取独占锁,acquire
方法的代码如下:
上面的方法执行流程如下:
-
首先调用
tryAcquire
尝试获取一次锁,若返回true
,表示获取成功,则acquire
方法将直接返回;若返回false
,则会继续向后执行acquireQueued
方法; -
tryAcquire
返回false
后,将执行acquireQueued
,但是这个方法传入的参数调用了addWaiter
方法; -
addWaiter
方法的作用是将当前线封装成同步队列的节点,然后加入到同步队列的尾部进行排队,并返回此节点; -
addWaiter
方法执行完成后,将它的返回值作为参数,调用acquireQueued
方法。acquireQueued
方法的作用是让当前线程在同步队列中阻塞,然后在被其他线程唤醒时去获取锁; -
若线程被唤醒并成功获取锁后,将从
acquireQueued
方法中退出,同时返回一个boolean
值表示当前线程是否被中断,若被中断,则会执行下面的selfInterrupt
方法,响应中断;下面我们就来具体分析这个方法中调用的几个方法的执行流程。首先第一个
tryAcquire
方法:
可以看到,这个方法的实现仅仅只是抛出了一个异常。我们之前提过,AQS
是基于模板方法设计模式实现的,在其中定义了许多模板方法,在模板方法中会调用一些没有实现的方法,这些方法需要使用者根据自己的需求实现。而acquire
方法就是一个模板方法,其中调用的tryAcquire
方法就是需要我们自己实现的方法。tryAcquire
的作用就是尝试修改state
值,也就是获取锁,若修改成功,则返回true
,否则返回false
。它的实现需要根据AQS
的子类具体分析,比如ReentrantLock
中的Sync
,这里我就不详细叙述了,后面写一篇专门讲ReentrantLock
的博客。下面来看看addWaiter
的源码:
以上就是addWaiter
方法的实现过程,我在代码中使用注释对每一步进行了详细的解析,它的执行过程大致可以总结为:将新线程封装成一个节点,加入到同步队列的尾部,若同步队列为空,则先在其中加入一个默认的节点,再进行加入;若加入失败,则使用死循环(也叫自旋)不断尝试,直到成功为止。这个过程中使用CAS
保证了添加节点的原子性。下面看看acquireQueued
方法的源码:
以上就是acquireQueued
方法的源码分析。这个方法的作用可以概括为:让线程在同步队列中阻塞,直到它成为头节点的下一个节点,被头节点对应的线程唤醒,然后开始获取锁,若获取成功才会从方法中返回。这个方法会返回一个boolean
值,表示这个正在同步队列中的线程是否被中断。
到此,获取独占锁的实现就分析完毕了。需要注意的是,这些过程中使用的compareAndSetXXX
这种形式的方法,都是基于CAS
机制实现的,保证了这些操作的原子性。
(2)释放锁的实现
分析完获取独占锁的代码后,我们再来看看释放锁的实现。释放独占锁是通过release
方法实现的:
以上就是同步队列中头节点对应的线程释放锁的过程。release
也是一个模板方法,其中通过调用tryRelease
尝试释放锁,而tryRelease
也需要使用者自己实现。在之前也说过,头节点释放锁时,需要唤醒它的下一个节点对应的线程,让这个线程不再等待,去获取锁,而这个过程就是通过unparkSuccessor
方法实现的。
2|62.6 共享锁的获取与释放过程
前面提到过,AQS
不仅仅可以用来实现独占锁,还可以用来实现共享锁,下面我们就来看看AQS
中,有关共享锁的模板方法的实现。首先是获取共享锁的实现,在AQS
中,定义了acquireShared
方法用来获取共享锁:
可以看到,这个方法比较简短。首先调用tryAcquireShared
方法尝试获取一次共享锁,即修改state
的值,若返回值>=0
,则表示获取成功,线程不受影响,继续向下执行;若返回值小于0
,表示获取共享锁失败,则线程需要进入到同步队列中等待,调用doAcquireShared
方法。acquireShared
方法也是AQS
的一个模板方法,而其中的tryAcquireShared
方法就是需要使用者自己实现的方法。下面我们来看看doAcquireShared
方法的实现:
doAcquireShared
方法的实现和获取独占锁中的acquireQueued
方法很类似,但是主要有一点不同,那就是线程在被唤醒后,若成功获取到了共享锁,还需要判断共享锁是否还能被其他线程获取,若可以,则继续向后唤醒它的下一个节点对应的线程。下面再看看释放共享锁的代码,释放共享锁时通过方法releaseShared
:
releaseShared
也是一个模板方法,它通过调用使用者自己实现的tryReleaseShared
方法尝试释放锁,修改state
的值,若返回true
,表示修改成功,则继续向下调用doReleaseShared
唤醒head
的下一个节点对应的线程,让它开始尝试获取锁;若修改state
失败,则返回false
。
2|72.7 使用AQS实现一个锁
介绍完上面的内容,下面我们就来基于AQS
实现一个自己的同步器,或者说锁。我们需要实现的锁要求如下:
实现一个锁,它是一个共享锁,但是每次至多支持两个线程同时获取锁,若当前已经有两个线程获取了锁,则其他获取锁的线程需要等待。
实现代码如下:
以上就实现了一个支持两个线程同时允许的共享锁,下面我们通过一个测试代码来测试效果:
以上测试代码运行后,在每两个分割行之间,最多不会输出超过两个线程的名称,线程名称的输出将会以两个一队出现。我的输出结果如下:
2|82.8 AQS如何实现线程等待
在研究AQS
的过程中,我一直有这个疑惑——AQS
如何让线程阻塞,直到最后才知道有一个叫LockSupport
的工具类。这个工具类定义了很多静态方法,当需要让一个阻塞,或者唤醒一个线程时,就可以调用这个类中的方法,它的底层实现是通过一个sun.misc.Unsafe
类的对象,unsafe
类的方法都是本地方法,由其他语言实现,这个类是给不支持地址操作的Java
,提供的一个操作内存地址的后门。
AQS
中通过以下两个方法来阻塞和唤醒线程:
- LockSupport.park():阻塞当前线程;
- LockSupport.unpark(Thread thread):将参数中传入的线程唤醒;
前面讲解AQS
的代码中,用到了方法unparkSuccessor
,它的主要作用就是唤醒当前节点的下一个节点对应的线程,我们可以看看它的部分实现:
3|0三、总结
其实AQS
还支持一些其他的方法,比如说在获取锁时设置超时时间等,这些方法的实现与上面介绍的几种大同小异,限于篇幅,这里就不进行叙述了。以上内容对AQS
的实现原理以及主要方法的实现做了一个比较细致的介绍,相信看完之后会对AQS
有一个比较深入的理解,但是想要理解以上内容,需要具备并发的一些基础知识,比如说线程的状态,CAS
机制等。最后希望这篇博客对需要的人有所帮助吧。
4|0四、参考
- 《Java并发编程的艺术》
- https://www.cnblogs.com/zyrblog/p/9866140.html
__EOF__

本文链接:https://www.cnblogs.com/tuyang1129/p/12670014.html
关于博主:在互联网洋流中垂死挣扎,但依旧乐观的Java小菜鸟一枚!
版权声明:转载博客请注明出处,并附上原文链接!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构