AQS源码解析
文章目录
一、AQS简介
AQS(AbstractQuenedSynchronizer)是抽象的队列式同步器框架,是除了java自带的synchronized关键字之外的锁机制。其底层采用乐观锁,大量使用了CAS操作,同时采用自旋方式重试,以实现轻量级和高效地获取锁。
- AQS的核心思想
如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
- CLH队列
CLH(Craig,Landin,and Hagersten)队列:一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系。 AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配。
总的来说,AQS维护了一个CLH队列,以及一个用volatile修饰的state(共享资源),其中state由线程通过CAS去改变。其大致框架如下图所示(图片来源于《Java并发之AQS详解》):
AQS本身是基于模板方法模式设计的,在使用时无需关注具体的维护和实现(如获取资源失败、入队、出队、唤醒等),只需要重写获取和释放共享资源state的方法即可。目前AQS定义了两种资源共享方式:Exclusive(独占,如:ReentrantLock)和Share(共享,如:Semaphore、CountDownLatch)。
主要的重写方法有:
- tryAcquire(int):独占式尝试获取资源
- tryRelease(int):独占式尝试释放资源
- tryAcquireShared(int):共享式获取同步状态
- tryReleaseShared(int):共享式释放同步状态
- isHeldExclusively():是否在独占模式下被线程占用,通常只有用到Condition时才需要实现
注意:
这些方法都没有定义成abstract,这保证了独占模式下只用实现tryAcquire和tryRelease,共享模式下只用实现tryAcquireShared和tryReleaseShared。
目前实现了AQS的组件有:ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch、CyclicBarrier等。
二、AQS的节点类及state状态
- Node节点类
static final class Node {
// waitStatus值
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
// 节点的状态
volatile int waitStatus;
// 节点的前驱
volatile Node prev;
// 节点的后继
volatile Node next;
// 节点封装的线程
volatile Thread thread;
}
// 头节点和尾节点
private transient volatile Node head;
private transient volatile Node tail;
注意:
head头节点是一个哑结点或者是当前持有锁的线程,真正的等待线程是从第二个节点开始的。
- waitStatus表示当前节点的等待状态,总共只会有五种取值:
- CANCELLED:当前节点已取消,也是唯一大于0的状态。
- SIGNAL:表示当前节点的后继节点在等待唤醒,在后继节点找到安全点时,会更新其前驱的状态为此。
- CONDITION:节点调用了Condition的await(),正在等待。
- PROPAGATE:共享模式下,可以唤醒多个后继节点
- 0:初始默认状态,在源码的注释中有提到。
- 同步状态
private volatile int state;
该属性表示了锁的状态,state为0表示锁没有被占用,state大于0表示当前已有线程持有该锁,大于1则说明存在重入的情况。
- state的方法
// getter/setter方法
protected final int getState() {
return state;
}
protected final void setState(int newState) {
state = newState;
}
// state 通过 CAS 进行设置
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
- 持有锁的线程信息
// 当前持有锁的线程
private transient Thread exclusiveOwnerThread;
该属性来源于AQS继承的抽象类AbstractOwnableSynchronizer,用于记录占用锁的线程是哪一个。在监视器锁中,通过ObjectMonitor对象的_owner属性,而在AQS中,则通过exclusiveOwnerThread属性。
三、主要方法源码解析
3.1 acquire(int)——独占模式
// 独占模式的acquire,忽略中断
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquire()方法是AQS的核心方法之一,它是独占模式下线程获取共享资源的顶层入口,在获得资源之前,是不响应中断的。它的具体流程如下:
- 先调用自定义的tryAcquire() 获取资源,成功则结束整个逻辑。
- 没能直接获取资源,调用addWaiter() 方法通过CAS操作将当前包装线程的节点进行尾插。
- 执行acquireQueued() 逻辑,它主要是实现节点进入队列,安全地进入阻塞状态,直到其它线程唤醒,并不断尝试获取资源。每次被唤醒,它都会进入循环,如果自己是第二个节点(即head后面一个),才能尝试请求资源,只有获取到资源后才会返回。如果在等待过程中被中断过,会返回true。
- acquireQueued()调用的shouldParkAfterFailedAcquire() 保证线程被阻塞前能够找到安全的前驱,并设置前为SIGNAL。
- acquireQueued()调用的parkAndCheckInterrupt()让线程进入阻塞状态,如果有中断,返回true。
- 如果线程在等待过程中被中断,它是不响应的,但会记录中断。在获取资源后才调用selfInterrupt() 进行自我中断。
- acquire方法的基本流程图如下所示(图片来源于《Java并发之AQS详解》以及《逐行分析AQS源码》):
- tryAcquire(int)
// 该逻辑需要自定义实现
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
- addWaiter(Node) 及 enq(Node)
由下面的代码,可以发现该方法不仅保证是CAS入队,也负责在队列为空时, 初始化队列,即队列是延时初始化(懒加载) 的。
// 节点入队
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 通过 CAS 进行一次快速尝试,尾插入队
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 1、队列是个空队列
// 2、未能 CAS 快速插入,则通过 enq() 入队
enq(node);
return node;
}
// 循环 CAS 入队
private Node enq(final Node node) {
// 循环 CAS 直到入队成功
for (;;) {
Node t = tail;
// 队列为空,创建一个空节点作为 head
// 然后进入下一个循环进行尾插
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 队列不空,CAS 尾插
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
尾分叉问题:
在enq进行尾插,三个步骤不是原子操作。其中第一步容易成功,第二步CAS在并发条件下有可能失败,第三步只有在第二步成功的条件下才执行。
所以,当有大量的线程在同时入队时,只有一个线程能完整地完成这三步,而其它线程只能完成第一步(即,node.prev = t),于是就出现了尾分叉。
基于上述原因,一个节点如果能入队(完成了第二步),则它的prev属性一定是有值的,但是它的next属性可能暂时还没有值。因此,在AQS相关的源码中,常常会出现从尾节点逆向遍历链表。
- acquireQueued(Node, int)
线程在队列中阻塞,并且不断尝试获取资源,成功则返回,记录中断
final boolean acquireQueued(final Node node, int arg) {
// 标记资源是否获取成功
boolean failed = true;
try {
// 记录阻塞过程中的中断
boolean interrupted = false;
// 无限循环,直到获取成功
for (;;) {
// 拿到前驱节点
final Node p = node.predecessor();
// 只有前驱是 head 时,才有资格去尝试获取资源
if (p == head && tryAcquire(arg)) {
// 获取成功,将当前节点设为头节点
// 即队列的头节点为当前运行的线程
setHead(node);
p.next = null; // help GC
failed = false;
// 返回中断信息
return interrupted;
}
// 没能拿到资源时,找到安全前驱节点,并进入阻塞状态,直到被唤醒
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
// 记录阻塞过程中是否被中断
interrupted = true;
}
} finally {
// 没有成功获取资源,则取消结点在队列中的等待
if (failed)
cancelAcquire(node);
}
}
- shouldParkAfterFailedAcquire(Node, Node)
- 线程阻塞前的状态检查。
- SIGNAL:一个节点的waitStatus为SIGNAL(由后继节点设置),说明它的后继节点已经被挂起了,当释放锁或者放弃获取锁时,该节点还要唤醒它的后继节点。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 前驱的状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 如果前驱直接是 SIGNAL,直接返回,并进行阻塞操作
return true;
// 前驱状态为 CANCELLED,需要找到还在队列中等待的有效节点
if (ws > 0) {
// 循环一直查找到有效节点为止
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 通过 CAS 设置前驱的状态为 SIGNAL
// 保证前驱释放资源时能够唤醒自己
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
- parkAndCheckInterrupt()
private final boolean parkAndCheckInterrupt() {
// 调用 park() 使线程阻塞,等待 unpark() 或 interrupt() 唤醒自己
LockSupport.park(this);
// 返回阻塞过程中的中断信息
return Thread.interrupted();
}
3.2 release(int)——独占模式
此方法是独占模式下线程释放共享资源的顶层入口,它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其它线程来获取资源,这也是unlock()的语义。
即,它是根据tryRelease()的返回值来判断该线程是否已经完全释放掉资源了。所以自定义实现tryRelease()的时候一定要明确这一点。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
// 当一个 head 节点的 waitStatus 为0,说明 head 后面没有在挂起等待中的后继节点了
// 只有 shouldParkAfterFailedAcquire 将前驱节点的 waitStatus 设为 Node.SIGNAL,否则默认为 0
if (h != null && h.waitStatus != 0)
// 唤醒等待队列里的下一个线程
unparkSuccessor(h);
return true;
}
return false;
}
- tryRelease()
一般情况下,tryRelease()都会成功的,因为线程来释放资源,则说明它肯定已经拿到独占资源了,是线程安全的,直接减掉相应量的资源即可(state-=arg)。
所以自定义操作时,也不需要CAS操作(例如ReentrantLock)。此外,如果已经彻底释放资源了(state=0),要返回true,否则返回false。
// 该逻辑需要继承的子类自定义实现
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
- unparkSuccessor(Node)
此方法用于唤醒等待队列中的下一个线程。
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
// 如果 head 的 waitStatus 比 0 小, 则直接将它设为 0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 通常情况下, 要唤醒的节点就是自己的后继节点
// 如果后继节点存在且也在等待锁, 那就直接唤醒它
// 找到下一个需要唤醒的结点 s
Node s = node.next;
// 如果后继节点为 null 或已取消,直接从后往前遍历
// 直到找到距离 head 节点最近的 ws<=0 的节点
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
// 只要waitStatus <= 0,就是有效节点
if (t.waitStatus <= 0)
s = t;
}
// 直接唤醒下一个线程
if (s != null)
LockSupport.unpark(s.thread);
}
在调用了unpark()方法以后,从parkAndCheckInterrupt()的挂起状态恢复,直接向下执行。在acquireQueued()中的无限循环中继续进行判断(if (p == head && tryAcquire(arg))并执行相应逻辑。
3.3 acquireShared(int)——共享模式
此方法是共享模式下线程获取共享资源的顶层入口。它会获取指定量的资源,获取成功则直接返回,获取失败则进入等待队列,直到获取到资源为止,整个过程忽略中断。
在共享模式下,当一个节点获取到了共享锁,就可以立刻唤醒其后继节点。这是因为共享锁可以被多个线程同时持有,一个锁获取到了,其后继的节点也可以直接来获取。因此,在共享模式下,在获取锁和释放锁时,都会唤醒后继节点。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
- tryAcquireShared()
是继承的子类需要自定义实现的方法,返回的是一个整型值。负值代表获取失败,0代表获取成功,但没有剩余资源,正数表示获取成功,且还有剩余资源,其它线程还可以去获取。
tryAcquireShared()尝试获取资源,不成功则通过doAcquireShared()进入等待队列,直到获取到资源。
- doAcquireShared()
逻辑与独占模式的acquireQueued()几乎一致,只是尾插放到里面一并执行。此外,下一个线程获取到资源后,如果还剩余资源,可以继续唤醒后面的线程。
private void doAcquireShared(int arg) {
// CAS 进行尾插
// 标注为是共享模式
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
// 只有当前节点是 head 的后继节点时,才能尝试获取锁
if (p == head) {
int r = tryAcquireShared(arg);
// 获取资源成功
if (r >= 0) {
// 将 head 指向自己,如果还有剩余资源可以再尝试唤醒之后的线程
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 这些逻辑与独占模式下相同
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
- setHeadAndPropagate()
相比于setHead()方法,在设置自身为head后,还会调用doReleaseShared() 去唤醒后面的线程。
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
// 将当前被唤醒的节点设为头结点 head
setHead(node);
// 如果资源还有剩余,继续唤醒后面的线程
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
3.4 releaseShared()——共享模式
此方法是共享模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果成功释放且允许唤醒等待线程,它会唤醒等待队列里的其它线程来获取资源。
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
- tryReleaseShared()
继承的子类需要自定义实现的方法。独占模式下的tryRelease()需要在完全释放掉资源(state=0)后,才能返回true;而共享模式下则不需要,线程在释放掉部分资源时就可以唤醒后继等待结点。
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
- doReleaseShared()——待续(可以参考这篇文章:《逐行分析AQS源码(3)——共享锁的获取与释放》)
该方法涉及到两处调用,它可能会被同一个head调用两次,也可能两次是不同的head(因为共享模式下,线程可能有多个)。
对于该方法,当head变化时,会回到循环中再次唤醒head的下一个节点。在当前节点唤醒后继节点之后,如果发现被唤醒后继节点已经成为了新的头节点,则会立即触发唤醒head节点的下一个节点的操作,继续新一轮的循环。
一直会持续到head没能变化时,才会退出。
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)
break;
}
}
四、同步类继承AQS的逻辑
同步类在进行实现时一般都会自定义一个同步器Sync,并将之定义为内部类。该同步器会继承AQS,同步类则对外提供接口。Sync只用自定义实现资源state的获取、释放方法(tryAcquire / tryRelelase),至于线程的排队、等待、唤醒等,上层的AQS都已经实现好了。