存在状态依赖性的类在并发操作时就会有同步问题, 保证这种类的线程安全的一种方式就是将状态操纵委派给另一个线程安全的状态维护类. AbstractQueueSynchonizer就是在JUC包中, 多种同步容器依赖的底层状态维护类.
在本系列的第一篇文章中, 我们自己实现了一个锁类, 这个锁类有两个组成部分, 一个原子性的状态成员, 和一个线程安全的等待队列, 这两个成员也正是大多数同步类的基本框架. AQS维护了一个volatile int state
和一个FIFO的等待队列, 其中state就是我们前面提到的状态依赖, 状态依赖在不同的同步容器中有着不同的语义, 例如在ReentrantLock
中, state表示的是是否被线程持有/持有线程的重入次数; 在Semaphore
中, state则表示剩余的可用资源的数量, 总之, 这是一个多线程共享的状态, 也因此标注为volatile
. 等待队列的节点包含了等待获取该状态的线程.
继承AQS的子类需要根据资源是否是独占需要实现以下方法
并在调用是AQS的子类时, 使用以下方法
排队, 阻塞, 队列管理这些工作, 则由AQS进行实现. 为了保证类的线程安全, 子类扩展的方法必须是线程安全的, 短小的和非阻塞的. 通常子类实现的要么是独占的要么是共享的, 但是也有例外, ReentrantReadWriteLock
中就既用到了共享的方法, 也用到的独占的方法, 将读写锁在同一个AQS子类中实现.
这里之所以没有把tryAcquire, tryRelease等方法定义成abstract,是因为独占模式下只用实现tryAcquire-tryRelease,而共享模式下只用实现tryAcquireShared-tryReleaseShared。如果都定义成abstract,那么每个模式也要去实现另一模式下的接口。
同时, AQS建议依赖AQS的同步工具应当将AQS的拓展类作为私有类, 同步工具用到的同步方法委派这个私有类的实例去完成, 而非同步工具直接继承AQS. 这样做的好处是避免将AQS的方法暴露给接口, 保证了同步工具接口的整洁性和AQS扩展类的安全性, 避免错误的使用了AQS的其他方法.
以下给出一个简单的基于AQS的闭锁的实现, 相比于CountDownLatch, 它只需要一个signal就可以激活, 并且是共享的.
class BooleanLatch {
// 一个继承了AQS的静态内部工具类
private static class Sync extends AbstractQueuedSynchronizer {
// state == 0表示没有收到信号, 保持关闭
boolean isSignalled() { return getState() != 0; }
// 如果没有收到信号, 返回负值, 意味着进入队列等待, 否则不做任何事(放行)
// 形参在这里没有含义
protected int tryAcquireShared(int ignore) {
return isSignalled() ? 1 : -1;
}
// 任何一个release表示收到信号, 所有等待线程和后续线程都可以通过acquire
protected boolean tryReleaseShared(int ignore) {
setState(1);
return true;
}
}
// 实际同步工具内包含一个私有的委托对象
private final Sync sync = new Sync();
public boolean isSignalled() { return sync.isSignalled(); }
// 将同步相关的工作委托给私有对象的发布方法完成
public void signal() { sync.releaseShared(1); }
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
}
再来看一个ReentrantLock.NonfairSync
// ReentrantLock中, state表示的状态是重入次数
static final class NonfairSync extends Sync {
// 用于lock.lock()
final void lock() {
// 快速模式
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
// 内部调用了aqs.acquire(arg)
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
abstract static class Sync extends AbstractQueuedSynchronizer {
/**
* Performs {@link Lock#lock}. The main reason for subclassing
* is to allow fast path for nonfair version.
*/
abstract void lock();
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 快速模式
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
protected final boolean isHeldExclusively() {
// While we must in general read state before owner,
// we don't need to do so to check if current thread is owner
return getExclusiveOwnerThread() == Thread.currentThread();
}
}
在上一小节的两个例子中, 可以看出AQS的设计是一种典型的模板模式, 通过子类重写方法达到在固定流程(稳定)中自定义判断条件(变化)的目的.
我们还是以ReentrantLock
的lock
和unlock
方法来一窥AQS的固定流程.
申请锁的流程:
释放锁的过程:
可以看到, AQS在整个过程中处理了无法获得锁时的线程入队和释放锁时的等待线程的释放的工作. 这部分工作也是AQS的一个难点. 为了维护这样一个队列, 我们来看一下AQS做了哪些工作.
从aquire开始看, 这个函数完成的主要逻辑是首先调用tryAcquire
尝试获取状态, 如果这一步获取了并且修改了状态, 就结束了; 如果没有成功获取, 则会将当前线程封装成一个Node加入队列进行排队, 同时让该进程进入waiting状态, 整个过程不响应中断.
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这个函数的工作是将线程封装成对应模式的Node, 并加入等待队列中. 等待队列是个带头结点的双向链表, AQS维护了head和tail两个Node对象, 其中tail
和head
都是volatile
, 加入的方式首先尝试快速加入, 如果快速加入失败, 则会调用enq
方法进行普通的加入队列. 最终返回当前线程所构造的节点.
private Node addWaiter(Node mode) {
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;
// 尝试使用CAS设置队列尾, 如果队列已经是存在的, 并且没有竞争发生, 则设置成功
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 否则回退到普通的队列加入
enq(node);
return node;
}
enq
方法负责两种情况, 一种是初始化等待队列, 一种是在竞争场景下添加一个节点. 如果tail是null, 说明当前没有等待队列, 则在head位置用一个新节点替换null; 如果tail不是null, 则说明是在竞争状态下添加node, 则不断尝试用node替换当前tail上的节点t, 直到替换成功, 成功后返回插入前的尾节点
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
方法传入的是当前线程所构造的已经入队的node, 以及当前线程acquire的arg. 该方法的作用是管理入队后的线程的acquire工作, 包括线程的阻塞和被唤醒后的抢占, 以及中断检查. 返回的是过程中是否被中断过, 但是整个过程并不响应中断, 只有在获得资源之后, 才将中断补上.
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 并发抢占所用到的循环
for (;;) {
final Node p = node.predecessor();
//如果前驱是head,即该结点已成老二,那么便有资格去尝试获取资源(可能是老大释放完资源唤醒自己的,当然也可能被interrupt了)
if (p == head && tryAcquire(arg)) {
// 成功acquire后
setHead(node);//拿到资源后,将head指向该结点。所以head所指的标杆结点,就是当前获取到资源的那个结点或null
p.next = null; // setHead中node.prev已置为null,此处再将head.next置为null,就是为了方便GC回收以前的head结点。也就意味着之前拿完资源的结点出队了!
failed = false;
return interrupted; // 返回当前中断flag的状态
}
// 没有成功acquire则要考虑是否阻塞当前线程
if (shouldParkAfterFailedAcquire(p, node) && // 判断是否应该阻塞
parkAndCheckInterrupt()) // 注意和这个位置, 实际上是先阻塞, 唤醒后再检查是否被中断
// 如果被中断了, flag改成true
interrupted = true;
}
} finally {
// finally被运行有两种情况,
// 一是predecessor抛出异常, 此时failed为true,
// 二是执行到return在返回前跳到这里, failed为false
if (failed)
// 如果等待过程中没有成功获取资源(如timeout,或者可中断的情况下被中断了),那么取消结点在队列中的等待
cancelAcquire(node);
}
}
上一个方法通过调用该方法来判断当前线程是否应当阻塞, 我们先来看下waitStatus的含义, 在Node中定义了waitStatus的几种状态
上面这些状态可以看出来大于0的是无效状态, 小于0的是有效状态, shouldParkAfterFailedAcquire
方法的工作, 首先判断当前节点的前驱节点是否是signal状态的, 如果是的话, 意味着它会在自身release的时候通知自己, 则可以安心park了; 如果不是的话, 分两种情况, 一种是前驱节点是有效状态, 这种时候通过CAS把它调整为SIGNAL, 在下一轮CAS中就可以park了; 另一种是前驱节点是无效状态, 这种情况, 当前节点之前的无效节点会被删除, 当前节点移到第一个有效节点后面.
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;//拿到前驱的状态
if (ws == Node.SIGNAL)
//如果已经告诉前驱拿完号后通知自己一下,那就可以安心休息了
return true;
if (ws > 0) {
/*
* 如果前驱放弃了,那就一直往前找,直到找到最近一个正常等待的状态,并排在它的后边。
* 注意:那些放弃的结点,由于被自己“加塞”到它们前边,它们相当于形成一个无引用链,稍后就会被保安大叔赶走了(GC回收)!
*/
do {
// 这里不需要考虑找到null么
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//如果前驱正常,那就把前驱的状态设置成SIGNAL,告诉它拿完号后通知自己一下。有可能失败,人家说不定刚刚释放完呢!
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
parkAndCheckInterrupt就比较简单了, 将当前线程阻塞, 下次恢复时返回是否是因为中断导致的被唤醒
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
到这里, 独占不予许中断的acquire模式就基本分析完了, 其整体的流程为:
acquireQueued
负责已经在等待队列中的节点的资源获取, 在自旋循环中, 判断自己是否可以抢占资源, 并尝试抢占资源, 如果抢占成功则完成, 抢占失败则在等待队列中通过shouldParkAfterFailedAcquire
找到安全的休息点将自身阻塞, 等待下一次唤醒release对应的语义是独占模式下的释放申请的资源, 方法的作用是尝试释放资源, 并通知后继节点.
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head; // 获取头结点
if (h != null && h.waitStatus != 0) // 如果头结点不为空并且状态不为零, 说明后面有节点将其状态更新过
unparkSuccessor(h);
return true;
}
return false;
}
该方法首先将当前节点的waitStatus复原, 找到后面第一个有效的后继节点, 对其线程唤醒.
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
这里需要联系acquireQueued
方法一起看, 假设等待队列中有线程放弃了, 在该方法中, 就会跳过这个线程找到它后面的有效的节点进行唤醒, 节点被唤醒后发现if (p == head && tryAcquire(arg))
判断失败, 则进入shouldParkAfterFailedAcquire
, 将当前这个节点调整到第一个有效节点的后面, 也就移到了头结点后面, 这样在下一轮CAS中, 这个后继节点就可以去尝试获取资源了.
到这里, 互斥的不允许中断的模式的acquire和release就分析的差不多了, 我们接着看一个允许中断了例子.
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
这个方法和之前的acquire
的区别, 首先是把加入等待队列等工作委托给了doAcquireInterruptibly
, 其次是在方法开始的位置对线程是否已经报了中断进行了检查, 此外doAcquireInterruptibly
方法也有可能抛出InterruptedException
.
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE); // 将线程封装进节点, 加入队列
// 和acquireQueued方法几乎一致
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException(); // 注意在acquireQueued中仅标注是否发生过中断, 但在这个方法中会抛出这个中断异常
}
} finally {
if (failed)
// 遇到当前节点是头结点或者抛出中断异常时, fail是为true的, 会取消该节点的调度
cancelAcquire(node);
}
}
从上面的acquireInterruptibly
和之前的acquire
方法的比较来看, 可以看出允许中断和不允许中断在细节实现上的差别, 但是总体流程上是几乎一致的.
上面分析的都是互斥模式的资源获取与释放, 接下来我们继续看一看AQS中共享模式下的流程.
首先还是看顶层的这个acquire
方法, 对于需要使用共享模式的同步器的需求来说, 在实现AQS的子类的时候, 重写tryAcquireShared
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0) //返回负值表示获取资源失败
doAcquireShared(arg); // 委托该方法再次尝试和入队等待
}
该方法的作用和acquireQueued
相似, 但是处于共享的非中断模式.
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) {
int r = tryAcquireShared(arg);
if (r >= 0) { // 成功获取资源
setHeadAndPropagate(node, r); // 差异, 传入的r的语义是剩余资源量
p.next = null; // help GC
if (interrupted)
// 和acquire有一点不一样的是, 在委托函数内
// 如果整个流程中出现过中断, 在获取资源后, 补上这个中断
selfInterrupt();
failed = false;
return;
}
}
// 同样是资源获取失败后, 找到安全休息点, 将前继节点设为signal 然后阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
// 如果中断或者自身是头结点
cancelAcquire(node);
}
}
整个流程依然是相似的, 和acquireQueued
的区别在于用setHeadAndPropagate
代替了setHead`, 那我们接着看下这个方法中做了什么.
这个方法和tryAcquireShared
共同构成了完成共享模式的落地, 后者返回的语义是剩余的资源数量, 前者根据剩余资源数, 决定是否向后传播SIGNAL, 唤醒等待线程.
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node); // 将当前节点设置为head
/*
*
* The conservatism in both of these checks may cause
* unnecessary wake-ups, but only when there are multiple
* racing acquires/releases, so most need signals now or soon
* anyway.
* 满足以下条件尝试signal下一个队列中的节点:
* - 调用者要求传播(propagate > 0)
* - 头结点的状态为PROPAGATE或SIGNAL(setHead前或后)
* 并且后继节点在共享模式下等待或后继节点为空
*
* 如果资源还有剩余量,继续唤醒下一个邻居线程
*/
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
// 如果头结点是SIGNAL说明后面已经有节点等待在该节点上, 则唤醒下一个节点;
// 如果头结点是0, 则调整为propogate状态, 保证下一次release时, 传播得以继续.
}
}
这个方法实现了共享模式下的release, 作用包括signal后继节点和保证release的传播. 具体的, 当release被调用时,即使有其他的acquire/relesae在并发执行, 也要保证release的传播, 首先如果后继节点要求了signal则会unpark后继节点, 否则会将头结点设置为PROPAGATE来保证下一次触发release时, 传播能够继续(setHeadAndPropagate中的ws < 0)
private void doReleaseShared() {
// 循环是为了保证当该方法执行时, 有新节点并发的添加进来, 这时候可能修改当前头结点的状态为SIGNAL, 这种情况需要重新执行循环, unpark新加进来的节点.
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // 如果头结点发生了改变要重新设置传播
break;
}
}
最后看一下共享模式下的release的执行
这个方法是共享模式的下的资源释放的公共api, 它内部就是调用的上面的doReleaseShared
.
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
AQS定义了线程获取资源, 释放资源, 获取资源失败时的等待队列管理这些内容, 子类通过重写判断资源是否成功获取和释放的方法, 来使用AQS, 作为同步容器的成员变量, 委派进行同步的管理.
参考资料: