背景
队列同步器AbstractQueuedSynchronizer,简称AQS,是用来构建锁或者其他同步组件的基础框架,以下代码上的中文注释大多为源代码中注释的翻译。
介绍
AQS提供了如下五个用于重写的方法:
-
protected boolean tryAcquire(int arg) 独占式的获取同步状态,返回值为true成功,false失败。
-
protected boolean tryRelease(int arg) 独占式的释放同步状态,返回值为true成功,false失败。
-
protected int tryAcquireShared(int arg) 共享式的获取同步状态,返回值为大于0成功,小于0失败
-
protected boolean tryReleaseShared(int arg) 共享式的释放同步状态,返回值为true成功,false失败。
-
protected boolean isHeldExclusively() 检查当前同步器是否在独占模式下被线程占用。
在自定义同步组件时,将会同步器提供的模板方法,如下为(部分)模板方法:
-
void acquire(int arg) 独占式的获取同步状态,阻塞
-
void acquireInterruptibly(int arg) 独占式的获取同步状态,阻塞,响应中断
-
boolean tryAcquireNanos(int arg, long nanosTimeout) 在acquireInterruptibly方法之上增加了超时等待,成功获取返回true,失败返回false
-
void acquireShared(int arg) 共享式的获取同步状态,阻塞。允许多个共享式的线程获取到同步状态
-
void acquireSharedInterruptibly(int arg) 与acquireShared类似,响应中断
-
boolean tryAcquireSharedNanos(int arg, long nanosTimeout) 在acquireSharedInterruptibly方法之上增加了超时等待,成功获取返回true,失败返回false
-
boolean release(int arg) 独占式的释放同步状态,会在释放同步状态之后,唤醒同步队列中第一个节点包含的线程
-
boolean releaseShared(int arg) 共享式的释放同步状态
-
Collection<Thread> getQueuedThreads() 获取等待在同步队列上的线程集合
源码解析
首先要先说明AQS中的两个队列:同步队列和等待队列
同步队列是用于存放获取同步状态失败的节点;等待队列是AQS中对Condition接口实现的一个内部类ConditionObject中的队列,用于存放等待获取监视器的节点。节点源码如下:
/**
* AQS内部维护了一个同步队列,Node就是同步队列的节点类,结构为:
* +------+ prev +-----+ +-----+
* head | | <---- | | <---- | | tail
* +------+ +-----+ +-----+
* 内部有一个head节点和tail节点,节点中存放前后节点的指针
*/
static final class Node {
/** 标记该节点处于共享模式下等待 */
static final Node SHARED = new Node();
/** 标记该节点处于独占模式下等待 */
static final Node EXCLUSIVE = null;
/** waitStatus的值,表示线程处于退出状态 */
static final int CANCELLED = 1;
/** waitStatus的值,表示当前节点被释放后,后续节点的线程需要被唤醒 */
static final int SIGNAL = -1;
/** waitStatus的值,表示线程等待在Condition上(即监视器) */
static final int CONDITION = -2;
/** waitStatus的值,表示后续的共享式获取同步状态不需要等待在Condition上,并传播 */
static final int PROPAGATE = -3;
/**
* 以下为节点的英文介绍:
* Status field, taking on only the values:
* SIGNAL: The successor of this node is (or will soon be)
* blocked (via park), so the current node must
* unpark its successor when it releases or
* cancels. To avoid races, acquire methods must
* first indicate they need a signal,
* then retry the atomic acquire, and then,
* on failure, block.
* CANCELLED: This node is cancelled due to timeout or interrupt.
* Nodes never leave this state. In particular,
* a thread with cancelled node never again blocks.
* CONDITION: This node is currently on a condition queue.
* It will not be used as a sync queue node
* until transferred, at which time the status
* will be set to 0. (Use of this value here has
* nothing to do with the other uses of the
* field, but simplifies mechanics.)
* PROPAGATE: A releaseShared should be propagated to other
* nodes. This is set (for head node only) in
* doReleaseShared to ensure propagation
* continues, even if other operations have
* since intervened.
* 0: None of the above
*
* The values are arranged numerically to simplify use.
* Non-negative values mean that a node doesn't need to
* signal. So, most code doesn't need to check for particular
* values, just for sign.
*
* 这个字段在普通的同步节点中会被初始化为0,在condition队列的节点中,会被初始化为CONDITION。
* 这个值使用CAS进行更新(或者在可能的情况下,无条件的使用volatile写)
*/
volatile int waitStatus;
/**
* 链接到当前节点/线程所依赖的用于检查waitStatus的前驱节点。在入队时分配,只有在
* 出队时设置为null(为了GC考虑)。
*/
volatile Node prev;
/**
* 链接到当前节点/线程release时会唤醒的后续节点
*/
volatile Node next;
/**
* 入队节点的线程,在初始化时设置,使用后设置为null
*/
volatile Thread thread;
/**
* 链接到下一个等待在等待队列上的节点, 或者一个特殊值SHARED。因为等待
* 队列仅允许独占式的访问,我们仅需要一个简单的链接队列去持有等待在
* condition上的node,它们后续会被移动到阻塞队列中重新获取同步状态。
* 因为condition是独占式使用的,所以我们使用一个特殊值表示共享状态。
*/
Node nextWaiter;
/**
* 返回true则表示节点阻塞在共享模式
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
* 返回前驱节点,为空抛出NullPointerException
*
* @return the predecessor of this node
*/
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // 用于创建初始化的head节点或者SHARED模式的标记
}
Node(Thread thread, Node mode) { // 用于添加队列的后续节点
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // 用于Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
接下来对每个模板方法的源码进行分析:
void acquire(int arg) 模板方法
/**
* 独占式的获取同步状态,忽略中断。实现方式是至少执行一次tryAcquire方法(需要我们
* 自己实现的),如果tryAcquire返回true则成功,否则线程将阻塞,可能会重复多次加锁
* 与解锁,直到执行tryAcquire返回true。这个方法可以用于实现Lock的lock方法。
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
可以看到,如果第一次执行tryAcquire成功,则方法直接返回,如果失败,则会执行acquireQueued方法。我们可以看到,acquireQueued方法的参数中传入的是addWaiter方法的返回值,所以这里我们先看一下addWaiter方法
Node addWaiter(Node mode)
/**
* 为当前的线程创建一个入队节点,并设置当前的节点模式
*
* @param mode Node.EXCLUSIVE 为独占模式, Node.SHARED 为共享模式
* @return 新创建的入队节点
*/
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 尝试快速入队,如果失败则使用完整的入队方法(即下面的enq方法)
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
addWaiter方法首先会为当前线程创建一个node,并设置一个mode。然后对这个节点尝试快速的入队:
1.如果tail节点不为空,则将tail节点cas替换成当前节点;
2.并将原tail节点的next节点设置为新创建的这个node;
3.如果失败,则调用完整的入队方法,即enq。
我们继续看一下enq方法
Node enq(final Node node)
/**
* 向队列中插入节点,如果需要则进行初始化
* @param node 需要插入的节点
* @return node的前驱节点
*/
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;
}
}
}
}
enq方法首先会判断tail节点是否为空,这也是addWaiter方法会判断的,只不过addWaiter方法在tail为空的情况下直接就交给enq方法处理了。如果tail节点为空,则表示当前的同步队列为空,需要对当前的同步队列进行初始化。初始化会cas的设置head节点,并将tail节点指向head节点,然后进入下一次循环。在队列初始化完之后,tail已经不为空了,那么会将插入node的prev指向原tail节点,并cas的替换tail节点为当前节点,如果替换成功,则将原tail节点的next节点指向当前节点,并返回原tail节点。值得注意的是,这里使用了CAS+自旋的方式保证了节点入队的成功,并只有在cas替换tail节点成功后,才会将原tail节点指向插入节点。
介绍完addWaiter方法之后,我们继续去看acquireQueued方法
boolean acquireQueued(final Node node, int arg)
/**
* 对已入队的线程独占且不响应中断式的获取同步状态,使用条件判断和阻塞方法。
*
* @param node the node
* @param arg the acquire argument
* @return 线程等待时被中断返回true
*/
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;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
在acquireQueued中,会自旋的判断node的前驱节点是否为head节点,如果是则尝试获取同步状态。成功或者则将node设置为head节点,失败则通过shouldParkAfterFailedAcquire判断线程是否需要阻塞。
boolean shouldParkAfterFailedAcquire(Node pred, Node node)
/**
* 在获取同步状态失败后检查并更新节点的状态。返回值为true则代表线程需要阻塞。
* 这是所有的获取同步的循环中都很重要的一个唤醒控制方法。参数pred == node.prev
*
* @param pred node's predecessor holding status
* @param node the node
* @return {@code true} 如果线程需要阻塞
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* 这个节点已经设置了release后被唤醒的状态,所以可以安全的阻塞
*/
return true;
if (ws > 0) {
/*
* 前驱节点已经处于CANCELLED状态,覆盖当前的前驱节点并重试
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus一定为0或者PROPAGATE。表示我们现在需要一个被唤醒的信号,
* 但现在还不需要被阻塞。调用者需要重试,以确保在阻塞前还无法获取同步状态
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
若需要阻塞,则调用parkAndCheckInterrupt阻塞并检查中断
boolean parkAndCheckInterrupt()
/**
* 一个提供阻塞并检查中断的简便方法
*
* @return {@code true} if interrupted
*/
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
boolean release(int arg)模板方法
/**
* 独占式的释放同步状态。实现基于当tryRelease方法返回true时,对一个
* 或者多个线程进行解锁。这个方法可以用于实现Lock的unlock方法。
*/
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
release方法相对于acquire方法就简单的多了。首先调用tryRelease尝试释放,如果此时头节点不为空且头节点的h.waitStatus != 0则说明同步队列中已有节点被阻塞,唤醒后继节点并返回true。若tryRelease返回false,则直接返回false。
void unparkSuccessor(Node node)
/**
* 如果有后继节点,唤醒它
*
* @param node the node
*/
private void unparkSuccessor(Node node) {
/*
* 如果节点状态为负,尝试清除预先的状态。这个操作允许失败。
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* 唤醒保存在后续节点的线程,正常情况下就是next节点。但如果它为
* null或者处于CANCELLED状态,则从tail节点向前遍历寻找不为null且
* 不处于CANCELLED状态的节点进行唤醒
*/
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);
}
当node的后继节点不为null或者CANCELLED状态时,是直接将其唤醒的。如果处于这两种状态,则会从tail向前遍历,并将需唤醒的节点t替换为一个可唤醒的节点。这里需要注意的是,遍历会一直循环到同步队列为空或者重新指向当前节点为止,所以被唤醒的节点还是空间上最接近node的节点。
void acquireShared(int arg) 模板方法
/**
* 共享式的获取同步状态,忽略中断。实现方式是第一次,并至少执行一次tryAcquireShared
* 如果失败则线程入队。可能会重复的加锁和加锁,直到执行tryAcquireShared成功
*/
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
方法内的逻辑比较简单,注释也比较详细,就不做过多的赘述。接下来看一下doAcquireShared方法
void doAcquireShared(int arg)
/**
* 共享式的获取同步状态并忽略中断
* @param arg the acquire argument
*/
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);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
可以看到,doAcquireShared和独占式获取节点同步状态的acquireQueued方法很类似,重复的代码就不再赘述,这里我们重点看下setHeadAndPropagate方法
void setHeadAndPropagate(Node node, int propagate)
/**
* 为队列设置head节点,如果并检查后继节点是否阻塞在共享模式。如果propagate > 0
* 或PROPAGATE状态被设置,则传播
*
* @param node
* @param propagate tryAcquireShared的返回值
*/
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
setHead(node);
/*
* 满足以下条件则唤醒后续的节点:
* 调用者表明需要传播,
* 或者已被之前的操作覆盖(h.waitStatus在setHead之前或者之后
* (注意:这里对waitStatus使用标记-检查是因为PROPAGATE状态
* 可能被转换成SIGNAL状态.)
* 并且
* 下一个节点在共享模式下阻塞,如果我们不知道它的状态,那么是因为它为null
*
* 这种保守方式的检查可能会导致不需要的唤醒,但只有当多个线程同时
* 竞争。所以无论如何大多数线程还是需要在现在或者未来被唤醒的。
*/
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
这里使用了相对保守的方式对节点的状态进行了检查。其实我们可以看到,如果是共享锁获取锁状态成功则propagate > 0。那么会判断head的后继节点是否为共享节点,如果是,则执行doReleaseShared方法。
void doReleaseShared()
/**
* 共享模式下的释放操作 -- 唤醒后续节点并确保传播继续。(注意,对独占模式,
* release仅相当于在需要被唤醒时,对head节点调用了unparkSuccessor方法)
*/
private void doReleaseShared() {
/*
* 确保释放的传递,即使其他线程调用了acquires/releases方法。通常情况下,
* 如果节点处于SIGNAL,则对head调用unparkSuccessor方法。但如果
* 不是的话,状态将被设置PROPAGATE去确保它将被释放,传播会继续。另外,
* 我们必须要对其进行循环,因为我们在操作时可能有新节点被添加。同时,不像
* 其他调用unparkSuccessor的方法,我们需要知道CAS设置状态是否失败,如果失败
* 则需要重试.
*/
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) // loop if head changed
break;
}
}
如果头节点的状态为SIGNAL,则CAS的将节点状态替换为0,并释放后继节点,失败则重试。确保传播的继续。
boolean releaseShared(int arg) 模板方法
/**
* 共享模式下释放。实现基于如果tryReleaseShared返回值为true时解锁一个或者多个线程
*/
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
doReleaseShared上面已解释过,如果等待队列中有后续节点,则唤醒。