一、概述
AQS 全称为 AbstractQueuedSynchronizer (队列同步器),这个类是其他许多同步类的基类,它是使用一个 volatile 修饰 int 类型成员变量表示某种状态(如:ReentrantLock用它来表示所有者线程已经重复获取锁的次数,Semaphore用它来表示剩余的许可数量,FutureTask用它来表示任务的状态),通过内置一个虚拟的 FIFO 队列来完成获取资源的线程的排队等待工作。在J.U.C 包中很多同步器都是基于 AQS 构建的,在基于 AQS 构建的同步器中,只可能在同一时刻发生阻塞,从而降低上下文切换的开销,并提高吞吐量。
二、接口说明
1、接口
AQS 的主要使用方式是继承,子类通过继承 AQS 类并实现它的某些方法来管理同步器状态,AQS 提提供类以下 3 个方法来访问或修改同步状态。
- getState():获取当前同步状态
- setState(int newState):设置当前同步状态
- compareAndSetState(int expect, int update):使用 CAS 设置当前状态,该方法能够保证设置状态的原子性。
通过 AQS 可以实现 独占式 和 共享式 两种通过同步器类。** 独占式是指一次只能有一个线程能够访问资源;共享式是指可以允许多个线程访问资源。**
实现独占式需要重写的方法
- boolean tryAcquire( int arg ):独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后在进行 CAS设置同步状态。
- boolean tryRelease( int arg):独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态。
实现共享式需要重写的方法
- int tryAcquireShared(int arg):共享式获取同步状态,返回大于等于0的值,表示成功,小于0的值表示失败
- boolean tryReleaseShared(int arg):共享式释放同步状态
其他方法
- boolean isHeldExclusively():当前同步器是否在独占模式下被占用,一般该方法表示是否被当前线程所独占。
2、模板方法
在实现自定义同步器时,推荐 子类被定义为同步组件的静态内部类,然后在自定义组件中调用 AQS 中的相关模板方法。AQS 实现的模板方法如下所示:
独占式模板方法
- void acquire(int arg):独占式获取同步状态,如果当前线程获取同步状态成功,则有该方法返回,否则,将会进入同步队列等待。
- void acquireInterruptibly(int arg):可中断的 独占式获取同步状态,如果未获取到同步状态进入同步队列中,,如果当前线程被中断,则该方法会抛出中断异常。
- boolean tryAcquireNanos(int arg, long nanosTimeout):在 acquireInterruptibly(int arg) 的基础上增加了超时限制,如果当前线程在超时时间内未获取到同步状态,则返回 false, 如果成功返回 true.
- boolean release(int arg):独占式的释放同步状态,该方法会在释放同步状态后,将同步队列中第一个节点的线程唤醒。
共享式模板方法
- void acquireShard(int arg):共享式获取同步状态,如果当前线程获取同步状态成功。
- void acquireShardInterruptibly(int arg):可中断的 共享式获取同步状态,如果未获取到同步状态进入同步队列中,,如果当前线程被中断,则该方法会抛出中断异常。
- boolean tryAcquireShardNanos(int arg, long nanosTimeout):在 acquireShardInterruptibly(int arg) 的基础上增加了超时限制,如果当前线程在超时时间内未获取到同步状态,则返回 false, 如果成功返回 true.
- boolean releaseShard(int arg):共享式的释放同步状态,该方法会在释放同步状态后,将同步队列中第一个节点的线程唤醒。
三、实现原理
1、同步队列
AQS 是依赖内部一个虚拟的 FIFO 双向队列同步队列来完成同步状态的管理,为什么说是虚拟的队列,因为 AQS 内部并非真的有个队列,而是在其内部定义了一个Node类,来维护了一个双向队列。Node类中主要属性 如下所示:
static final class Node {
/**
* 等待状态
*CANCELLED(1):由于在同步队列中等待的线程超时或者被中断,会触发变更为此状态,
* 进入该状态后的结点将不会再变化。
*
* SIGNAL(-1):后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,
* 将会通知后继节点,使后继节点的线程得以运行。
*
* CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,
* CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
*
* PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
*
* 0:新结点入队时的默认状态。
*/
volatile int waitStatus;
// 前驱节点,当节点加入同步队列时被设置
volatile Node prev;
// 后继节点
volatile Node next;
// 获取同步状态的线程引用
volatile Thread thread;
// 等待队列中的后继节点,如果当前节点是共享的,那么这个字段将是一个SHARED常量,
// 也就是说节点类型(独占和共享)和等待队列中的后继节点共用一个字段
Node nextWaiter;
}
通过 Node 节点是构成同步队列的基础,在 AQS 中拥有首节点 Node head 和尾结点 Node tail,没有成功获取同步状态的线程将被构造成 Node节点加入到队列的尾部,同步队列的基本结构如下所示:
2、独占式申请资源操作
由上图可知,在 AQS 中包含了 头结点 (head) 和尾结点( tail )两个节点类型的引用,当一个线程申请同步状态失败后,就会被构造成一个 节点( Node )插入到队列的尾部,而这个操作必须要保证是线程安全的,所以在 AQS 内部提供了一个基于 CAS 的设置尾结点的方法:boolean compareAndSetTail(Node expect, Node update)。入队操作如下所示:
查看申请锁源码如下:
2.1、acquire 方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
函数流程如下:
- tryAcquire() 尝试直接去获取资源,如果成功则直接返回(这里体现了非公平锁,每个线程获取锁时会尝试直接抢占加塞一次,而CLH队列中可能还有别的线程在等待)
- addWaiter() 将该线程加入等待队列的尾部,并标记为独占模式;
- acquireQueued() 使线程阻塞在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
- 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
2.2、tryAcquire(int arg) 方法
上面介绍了此方法尝试去获取独占资源。如果获取成功,则直接返回true,否则直接返回false。这也正是tryLock()的语义,还是那句话,当然不仅仅只限于tryLock()。如下是tryAcquire()的源码:
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
此方法默认抛出一个 异常,需要有 子类具体实现其方法。不在多说
2.3、addWaiter()
此方法是将当前线程 构造成一个Node 然后插入到 同步队列的队尾。源码如下所示:
private Node addWaiter(Node mode) {
// 把 当前线程构建成一个 Node 节点
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
// 尝试快速方式直接放到队尾。
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 如果快速插入失败,则进入 enq 方法通过自旋操作把 Node 节点插入队尾
enq(node);
return node;
}
private Node enq(final Node node) {
// 通过 CAS + 自旋的方式,将 Node 插入到同步队列的队尾
for (;;) {
Node t = tail;
if (t == null) { // 如果队列为空,则先创建一个 头结点,然后在插入Node节点
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) { // 通过CAS 操作插入到队尾
t.next = node;
return t;
}
}
}
}
在enq(Node node) 方法中,AQS 是通过“死循环” 来保证节点的正确添加,在 “死循环” 中只有通过 CAS 将节点设置成尾结点,否则将一直尝试,直到设置成功。
2.4、acquireQueued()
源码如下:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true; // 标记是否失败
try {
boolean interrupted = false; // 标记是否被中断
for (;;) {
final Node p = node.predecessor(); //获取到当前节点的前驱节点
//判断前驱节点释放为头结点,且尝试获取资源。
if (p == head && tryAcquire(arg)) {
setHead(node); // 果是头结点 且获取资源成功, 把当前节点设置为头结点
p.next = null; // help GC
failed = false;
return interrupted;
}
//如果自己可以休息了,就通过park()进入waiting状态,直到被unpark()。
// 如果不可中断的情况下被中断了,那么会从park()中醒过来,发现拿不到资源,从而继续进入park()等待。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 如果等待过程中没有成功获取资源(如timeout,或者可中断的情况下被中断了),那么取消结点在队列中的等待。
if (failed)
cancelAcquire(node);
}
}
通过 tryAcquire() 该线程获取资源失败,并且通过 addWaiter() 已经被放入等待队列尾部后,就会调用 acquireQueued() 方法,使当前线程在 “死循环” 中尝试获取同步状态,而只有前驱节点是头结点的节点才能够尝试获取同步状态,这是为什么呢?原因有如下两个:
- 头结点是成功获取到同步状态的节点,而头结点的线程释放了同步状态之后,将会唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否为头结点。
- 维护同步队列的 FIFO 原则。
该方法中,节点自旋获取同步状态的行为如下所示:
** 2.5、shouldParkAfterFailedAcquire**
此方法主要用于检查状态,看看自己是否真的可以去休息了(进入waiting状态,如果线程状态转换不熟),万一队列前边的线程都放弃了只是瞎站着,那也说不定,对吧!
shouldParkAfterFailedAcquire
整个流程中,如果前驱结点的状态不是SIGNAL,那么自己就不能安心去休息,需要去找个安心的休息点,同时可以再尝试下看有没有机会轮到自己拿号。
2.6、parkAndCheckInterrupt
如果线程找好安全休息点后,那就可以安心去休息了。此方法就是让线程去休息,真正进入等待状态。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); //调用park()使线程进入waiting状态
return Thread.interrupted(); // 返回当前线程的中断状态,并清空
}
2.7、小结
首先线程去尝试获取同步状态,如果获取成功则直接返回,处理自己的事情;如果失败则被构建成一个 Node 节点然后通过 自旋 + CAS 的方式把构建的节点插入到同步队列的队尾,插入成功后会判断其前驱节点是否为头结点,如果不是则线程进入等待状态,如果是会再次尝试申请同步状态,如果获取成功则把自己设置为头部节点,如果失败则进入等待状态。
说明:当前节点的前驱节点是头结点时,会出如下两种情况:
- 公平锁:如果是公平锁前驱节点是头结点,此时去获取同步状态,正常会成功。
- 非公平锁:在非公平锁请可下,当前获取同步状态时,有可能被其他节点插队了,所有存在获取同步状态失败的情况。
3、独占式释放资源操作
当前线程获取了同步状态并且执行了响应逻辑后,就需要释放同步状态,使得后续节点能够继续获取同步状态。通过调用 AQS 的 release(int arg) 方法可以释放同步状态,该方法在释放了同步状态之后,会唤醒其后继节点,进而使后继节点重新尝试获取同步状态。方法源码如下所示:
3.1、release() 方法
public final boolean release(int arg) {
//通过 tryRelease 尝试释放资源
if (tryRelease(arg)) {
Node h = head; //获取头结点
//判断头结点不null,为非初始状态,则执行唤醒操作
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
3.2、unparkSuccessor 方法
private void unparkSuccessor(Node node) {
//这里 node 当前释放资源的线程所在的节点。
int ws = node.waitStatus;
if (ws < 0)//置零当前线程所在的结点状态,允许失败。
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;//找到下一个需要唤醒的结点s
// 如果下一个节点为 null 或 已取消(超时或者被中断)
// 则从尾结点 从后往前查找
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev) // 从后向前找。
if (t.waitStatus <= 0)//从这里可以看出,<=0的结点,都是还有效的结点。
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);//唤醒
}
此方法不复杂。一句话概括:用unpark()唤醒等待队列中最前边的那个未被取取消的线程,这里我们也用 N 表示当前节点。此时,在结合 acquireQueued() ,N 被唤醒后,进入**if (p == head && tryAcquire(arg)) **的判断,如果不满足条件则会调用 shouldParkAfterFailedAcquire() 方法寻找下一个安全点,如果成功则执行自己的业务逻辑。
3.3、小结
资源的释放很简单,release() 方法是释放资源的入口,当前节点成功释放资源后就会唤醒后续节点,继续尝试获取资源。
4、共享式获取资源操作
4.1、acquireShared(int) 方法
此方法是共享模式下线程获取共享资源的顶层入口。它会获取指定量的资源,获取成功则直接返回,获取失败则进入等待队列,直到获取到资源为止,整个过程忽略中断。下面是acquireShared()的源码:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
4.2、tryAcquireShared(int arg)
这里tryAcquireShared()依然需要自定义同步器去实现。但是AQS已经把其返回值的语义定义好了:负值代表获取失败;0代表获取成功,但没有剩余资源;正数表示获取成功,还有剩余资源,其他线程还可以去获取。
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
4.3、doAcquireShared(int arg)
此方法用于将当前线程加入等待队列尾部休息,直到其他线程释放资源唤醒自己,自己成功拿到相应量的资源后才返回。源码如下所示:
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);//加入队列尾部
boolean failed = true;//是否成功标志
try {
boolean interrupted = false;//等待过程中是否被中断过的标志
for (;;) {
final Node p = node.predecessor();//前驱
if (p == head) {//如果到head的下一个,因为head是拿到资源的线程,此时node被唤醒,很可能是head用完资源来唤醒自己的
int r = tryAcquireShared(arg);//尝试获取资源
if (r >= 0) {//成功
setHeadAndPropagate(node, r);//将head指向自己,还有剩余资源可以再唤醒之后的线程
p.next = null; // help GC
if (interrupted)//如果等待过程中被打断过,此时将中断补上。
selfInterrupt();
failed = false;
return;
}
}
//判断状态,寻找安全点,进入waiting状态,等着被unpark()或interrupt()
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
此方法跟 独占式中的 acquireQueued 方法很类似,有一点区别是,如果线程被中断在补充中断时 独占式 的是在 acquireQueued 方法之外,而共享式的是放在了 doAcquireShared 方法中。
与独占式还有一点区别是,当节点被唤醒获取资源后,如果还有剩余资源,还会继续唤醒后续节点。如下图所示,当前节点 N0 获取到资源后,还剩余 2 个资源,那么当唤醒后续节点 N1 时,由于资源不足会继续阻塞到 N1 节点,不会在继续唤醒后续节点。
4.4、setHeadAndPropagate() 方法
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
setHead(node);//head指向自己
//如果还有剩余量,继续唤醒下一个线程
if (propagate > 0 || h == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
4.5、小结
至此,acquireShared() 也要告一段落了。让我们再梳理一下它的流程:
- tryAcquireShared() 尝试获取资源,成功则直接返回;
- 失败则通过 doAcquireShared() 进入等待队列 park(),直到被 unpark()/interrupt() 并成功获取到资源才返回。整个等待过程也是忽略中断的。
其实跟 acquire() 的流程大同小异,只不过多了个自己拿到资源后,还会去唤醒后继队友的操作。
5、共享式释放资源操作
5.1、releaseShared() 方法
上一小节已经把 acquireShared() 说完了,这一小节就来讲讲它的反操作 releaseShared() 。此方法是共享模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果成功释放且允许唤醒等待线程,它会唤醒等待队列里的其他线程来获取资源。源码如下所示:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) { //尝试释放共享资源
doReleaseShared(); //操作释放操作
return true;
}
return false;
}
5.2、doReleaseShared() 方法
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
unparkSuccessor(h);//唤醒后继
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
if (h == head)// head发生变化
break;
}
}
5.3、小结
共享模式的资源方法也很简单,一言以蔽之就是:释放掉资源后,唤醒后继。跟独占模式下不同的是,独占模式下在完全释放掉资源(state=0)后,才会返回true去唤醒其他线程,这主要是基于独占下可重入的考量;而共享模式的实质是控制一定量的线程并发执行,那么拥有资源的线程在释放掉部分资源时就可以唤醒后继等待结点。
四、示例
上面分析了 AQS 的原理及部分源码,接下来我们通过两个例子在巩固一下。
1、独占式示例
我们通过实现一个独占锁。
public class MonopolyLock implements Lock {
private final Sync sync = new Sync();
// 定义一个继承 AQS的静态内部类,自定义同步器
private static class Sync extends AbstractQueuedSynchronizer{
//独占式获取锁,当状态为 0 时获取锁,获取成功后设置独占线程为当前线程,返回true,否则返回 false
@Override
protected boolean tryAcquire(int arg) {
if(compareAndSetState(0, 1)){
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// 释放锁,设置同步状态为 0,同时清空独占线程
@Override
protected boolean tryRelease(int arg) {
if(getState() == 0){
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
// 判断锁释放处于被占有状态。
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
Condition newCondition(){
return new ConditionObject();
}
}
@Override
public void lock() {
sync.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
@Override
public void unlock() {
sync.release(1);
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
}
2、共享式示例
定义一个锁,实现每次有两个线程可以获取资源。具体代码如下所示:
public class TwinsLock implements Lock {
/**
* 继承 AbstractQueuedSynchronizer 实现一个同步器
*/
private static final class Sync extends AbstractQueuedSynchronizer{
//构造方法, count 默认资源数量
Sync(int count){
if (count < 0){
throw new IllegalArgumentException("count must large than zero.");
}
setState(count);
}
// 复写 共享 申请资源方法,通过 自旋 + CAS 的方式更新剩余资源数量
@Override
protected int tryAcquireShared(int arg) {
for (;;){
int current = getState();
int newCount = current - arg;
if(newCount < 0 || compareAndSetState(current, newCount)){
return newCount;
}
}
}
// 释放资源
@Override
protected boolean tryReleaseShared(int arg) {
for (;;){
int current = getState();
int newCount = current + arg;
if(compareAndSetState(current, newCount)){
return true;
}
}
}
}
// 定义一个同步器,资源数量为 2 个
private final Sync sync = new Sync(2);
@Override
public void lock() {
sync.acquireShared(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquireShared(1) > 0;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(time));
}
@Override
public void unlock() {
sync.releaseShared(1);
}
@Override
public Condition newCondition() {
return null;
}
}
示例都很简单,如有疑问,可以留言共同探讨。
五、写在最后
有人问过这样一个问题 “ AQS 为什么使用双向队列?”。个人认为原因如下:
- 因为线程在无法获取资源时,都是需要把构造的节点插入到队尾,使用双向队列,在插入是比价方便,直接操作队尾即可,无需遍历整个队列插入。
- 在线程 A 占有了资源之后,线程B 也来占有,线程B 需要判断前驱节点的状态确定是否需要阻塞。双向队列方便遍历。
- 双向扫描,当是否资源时,如果释放资源的后续节点为 null 或者被取消时,回查尾部遍历找到第一个为放弃的线程。