这是AQS 解读的第二篇,着重介绍同步队列的创建与操作;参见第一篇,同步队列在管程入口处;AQS构建的同步队列是一个双向链表;
文章目录
AbstractQueuedSynchronizer中的属性
/**
* Head of the wait queue, lazily initialized. Except for
* 等待队列的头节点,懒初始化。
* initialization, it is modified only via method setHead. Note:
* 除了初始化,它只能通过 setHead 方法修改。
* If head exists, its waitStatus is guaranteed not to be
* 注意:如果头节点存在,它的状态不应该是取消的;
* CANCELLED.
*/
private transient volatile Node head;
/**
* Tail of the wait queue, lazily initialized. Modified only via
* 等待队列的尾节点,懒初始化。
* method enq to add new wait node.
* 仅能通过方法 enq 添加一个新的等待节点而修改。
*/
private transient volatile Node tail;
/**
* The synchronization state.
* 同步状态
* 该字段在MESA管程模型中表示共享变量
*/
private volatile int state;
同步队列创建与入队
由于head和tail都是懒加载的,所以队列的创建是在入队时创建;
/**
* Creates and enqueues node for current thread and given mode.
* 为当前线程和给定的模式创建节点并入队
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
private Node addWaiter(Node mode) {
// 以给定的 mode,创建当前线程对应的节点
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
// 尝试快速的路径入队;失败时在使用完整的enq方法入队
Node pred = tail;
// 如果尾节点存在,则构建一个双向的链表,前尾节点的next节点指向当前节点,当前节点的prev指向前尾节点;
// 1,将当前节点的前置节点设置为尾节点
// 2,cas设置尾节点为当前节点
// 3,cas 成功,则将前尾节点的next指向当前节点
if (pred != null) {
node.prev = pred;
// 4,cas 可能失败,失败表示别的线程优先完成了尾节点的设置
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 调用完整的入队方法
enq(node);
return node;
}
private Node enq(final Node node) {
// 确保一定入队成功
for (;;) {
// 获取尾节点
Node t = tail;
// 尾节点不存在,则cas设置一个头节点,cas成功,尾节点也指向头节点,cas失败,下一次循环时 tail节点就最终不为空;即使别的线程设置了头节点cas成功,但是未来及执行赋值tail操作;其余的线程将始终循环cas,且都以失败返回,直到那个幸运的线程开始重新争取到时间片完成后续的赋值操作
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 尾节点存在,则将当前节点的前置节点设置为尾节点;也许在addwaiter中做过这一步,但是cas失败了,所有进入完整的enq方法,这时能保证node.prev永远正确的指向当前最新的尾节点
node.prev = t;
// 直到将当前节点添加到队尾后结束循环,完成入队
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
入队总结
尾插法入队+双向链表;
头节点调用的Node 的无参构造器,队列初始化时该节点也是尾节点;当新的节点入队时,该尾节点也是头节点的next指向新来的节点,新来的节点的prev节点指向该节点;同时将指针指向新来的尾节点;
同步队列的新来节点入队前,总是先设置自己的prev,再cas抢占尾节点,最后再将前尾节点的next指向自己;
记住这个顺序很重要;某些时候是无法通过next指针正确找到下一个节点,但可以通过前置节点找到上一个节点;
为什么入队,以及入队后发生了什么?
回答这个问题的一个简单方式,就是调研哪些地方调用了入队方法;因为是在 addWaiter方法里调用enq,所以我们先研究哪些地方调用了 addWaiter方法。
acquire资源入队
我们将调用addWaiter方法的方法名列出来:
public final void acquire(int arg);
private void doAcquireInterruptibly(int arg)throws InterruptedException;
private boolean doAcquireNanos(int arg, long nanosTimeout)throws InterruptedException ;
private void doAcquireShared(int arg) ;
private void doAcquireSharedInterruptibly(int arg)throws InterruptedException;
private boolean doAcquireSharedNanos(int arg, long nanosTimeout)throws InterruptedException;
可以很简单的看出,可以按照3种维度进行划分;
占用模式:独占获取或者共享获取
是否有时间限制:有时间限制获取和没有时间限制获取
是否响应中断:响应中断获取和不响应中断获取
时间限制以及是否响应中断的处理方式都很简单,这里我们不过多分析,因为本质上是很简单的api调用;这里我们按照是否独占来分析,选择简单独占模式 acquire 方法,以及简单共享模式doAcquireShared 方法分析
独占模式获取
/**
* Acquires in exclusive mode, ignoring interrupts. Implemented
* 独占模式下获取,忽略中断
* by invoking at least once {@link #tryAcquire},
* 以至少调用一次 tryAcquire 实现,
* returning on success. Otherwise the thread is queued, possibly
* 仅在成功时返回。否则, 线程排队,
* repeatedly blocking and unblocking, invoking {@link
* 可能会重复性的阻塞与解开,直到调用 tryAcquire 成功
* #tryAcquire} until success. This method can be used
* 这个方法可以用来实现lock
* to implement method {@link Lock#lock}.
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquire} but is otherwise uninterpreted and
* can represent anything you like.
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
/**
* Acquires in exclusive uninterruptible mode for thread already in
* 以独占且忽略中断的方式为一个已经在同步队列中的线程进行获取;
* queue. Used by condition wait methods as well as acquire.
* 在acquire与条件 wait方法时调用
* @param node the node
* @param arg the acquire argument
* @return {@code true} if interrupted while waiting
*/
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);
}
}
共享模式获取
与独占模式获取基本上大同小异,同样要入队,入队后判断是否可否获取资源,获取到资源则出队;无法获取资源则判断是否阻塞;
/**
* Acquires in shared uninterruptible mode.
* @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);
}
}
不同之处在于调用的子类实现的获取资源的方式不同,以及出队的方式不同;这里还有一个取消尝试获取中的节点,当发生错误时;我们将优先分析出队,再分析取消;
何时park线程
这个方法调用地方与调用addWaiter一致,即入队后都要判断是否应该park线程。
/**
* Checks and updates status for a node that failed to acquire.
* 一个在获取资源失败的节点检查并更新状态
* Returns true if thread should block. This is the main signal
* 返回true当节点应该阻塞时。这时一个主要的信号控制在所有获取循环中;
* control in all acquire loops. Requires that pred == node.prev.
* pred 应该是当前节点的前置节点
*
* @param pred node's predecessor holding status
* @param node the node
* @return {@code true} if thread should block
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// 前置节点状态为-1表示后继节点需要被唤醒
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* 这个节点已经设置了状态要求释放资源时通知它,
* to signal it, so it can safely park.
* 所以它可以安全的park
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* 前驱节点已经取消,跳过那些取消节点,并再尝试一次获取资源
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* 剩下的状态要么是0要么是传播-3.意味着我们需要设置一个信号,
* need a signal, but don't park yet. Caller will need to
* 但尚不park.调用方需要在parkk前尝试确保无法获取资源
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
条件队列入同步队列
我们已经研究完毕获取资源失败时入队,现在来研究直接调用 enq 方法入队的地方;老方法,我们将调用enq的方法列出来:
private Node addWaiter(Node mode);
final boolean transferForSignal(Node node);
final boolean transferAfterCancelledWait(Node node);
第一种场景我们已经调研过了,看剩下的两个方法;
transferForSignal 收到信号而转移
追踪这两个方法的调用处,我们发现只有两处调用这个方法,分别是 doSignal() 和 doSignalAll(),所以理解为收到signal信号后从条件队列转移到同步队列;
/**
* Transfers a node from a condition queue onto sync queue.
* 将一个条件队列的节点转移到同步队列。
* Returns true if successful.
* 成功返回true
* @param node the node
* @return true if successfully transferred (else the node was
* cancelled before signal)
*/
final boolean transferForSignal(Node node) {
/*
* If cannot change waitStatus, the node has been cancelled.
* cas 设置状态失败,表明当前节点取消
*/
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*
* Splice onto queue and try to set waitStatus of predecessor to
* 拼接到同步队列并尝试设置前置节点的waitStaus到-1,
* indicate that thread is (probably) waiting. If cancelled or
* 表明当前节点正在等待中。
* attempt to set waitStatus fails, wake up to resync (in which
* 如果前置节点取消了或者更改前者节点状态失败了,则唤醒当前节点的线程,
* case the waitStatus can be transiently and harmlessly wrong).
* 在这种场景下,当前节点的状态可能是瞬时无害的错误状态。
*/
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
transferAfterCancelledWait 取消await后转移
await方法是Condition中的方法,我们将在下一篇文章中讲述条件队列的await与唤醒;await取消也仅是指await有时间限制,当释放资源一定时间后,再次抢占资源;时间到期后,称为 cancelledAwait;
/**
* Transfers node, if necessary, to sync queue after a cancelled wait.
* 转移节点,如有需要,将一个取消等待的节点转移到同步节点
* Returns true if thread was cancelled before being signalled.
* 返回true,如果已经被取消在通知之前。
* @param node the node
* @return true if cancelled before the node was signalled
*/
final boolean transferAfterCancelledWait(Node node) {
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
enq(node);
return true;
}
/*
* If we lost out to a signal(), then we can't proceed
* 如过我们在signal方法竞争失败,我们需要阻塞
* until it finishes its enq(). Cancelling during an
* 直到当前节点入队。
* incomplete transfer is both rare and transient, so just
* 因为正在取消导致未完成的转移即少见又短暂(cas失败),所以旋转就好
* spin.
*/
while (!isOnSyncQueue(node))
Thread.yield();
return false;
}
我们分析了从条件队列节点转移到同步节点的逻辑,但是我们尚未分析条件队列的创建与入队,与出队;这些内容,我们放在第三篇讲解;这里,我们只要理解了进入同步队列的场景就好。
同步队列出队
在acquire 资源入同步队列时,可以看到在获取到资源后的出队操作。分别是setHead与setHeadAndPropagate
获取资源的出队操作
/**
* Sets head of queue to be node, thus dequeuing. Called only by
* 设置头节点未当前节点,实现出队。
* acquire methods. Also nulls out unused fields for sake of GC
* 仅能在获取资源方法中调用。清零不会使用的字段为了GC和压制不必要的信号与遍历
* and to suppress unnecessary signals and traversals.
*
* @param node the node
*/
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
/**
* Sets head of queue, and checks if successor may be waiting
* 设置头节点,并且检查后继接节点可能是一个共享模式下的节点。
* in shared mode, if so propagating if either propagate > 0 or
* 如果剩余资源大于0或者已经是传播模式
* PROPAGATE status was set.
*
* @param node the node
* @param propagate the return value from a tryAcquireShared
*/
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
/*
* Try to signal next queued node if:
* 尝试去唤醒下一个节点当:
* Propagation was indicated by caller,
* 传播由调用者指示,
* or was recorded (as h.waitStatus either before
* 或者被前一个操作记录(在setHead之前或者之后设置h.waitStatus)
* or after setHead) by a previous operation
* (note: this uses sign-check of waitStatus because
* (注意:这里检查waitStatus因为传播状态可能被设置为通知状态)
* PROPAGATE status may transition to SIGNAL.)
* and
* 且下一个节点是一个共享模式
* The next node is waiting in shared mode,
* or we don't know, because it appears null
*
* 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.
*/
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
// 如果下一个为空或者下一个节点是共享模式节点,尝试唤醒下一个节点
doReleaseShared();
}
}
阻塞与条件队列的节点被唤醒后,获取到资源后,会再次调用 acquireQueue 将自己从队列中出队;故我们分析完同步队列出队的场景。
取消正在获取的节点
观察上述6种获取的方法,在finally块中都会在发生错误时调用cancelAcquire;我们逐一分析可能出错的常规场景;
public final void acquire(int arg);
// 明显的是捕获到中断异常可能出错
private void doAcquireInterruptibly(int arg)throws InterruptedException;
// 明显的是捕获到中断异常可能出错
private boolean doAcquireNanos(int arg, long nanosTimeout)throws InterruptedException ;
private void doAcquireShared(int arg) ;
// 明显的是捕获到中断异常可能出错
private void doAcquireSharedInterruptibly(int arg)throws InterruptedException;
// 明显的是捕获到中断异常可能出错
private boolean doAcquireSharedNanos(int arg, long nanosTimeout)throws InterruptedException;
剩余的两种场景中,我们无法看出显而易见可能出错的场景,但所有的场景中都会调用 tryAcquire这一个子类实现的方式,很有可能子类实现时基于特殊的业务原因,导致调用报错,为了一个稳定的框架,所以有必要进行节点的取消;
/**
* Cancels an ongoing attempt to acquire.
* 取消一个正在进行获取的意图
* @param node the node
*/
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
// 将当前节点的线程设置为空
node.thread = null;
// Skip cancelled predecessors
// 跳过取消的前置节点
Node pred = node.prev;
// 只要前置节点的状态为2,都代表节点已经取消了,则找到那个尚未取消的前置节点
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// predNext is the apparent node to unsplice. CASes below will
// 前置节点的后继节点是我们要拆分的节点;
// fail if not, in which case, we lost race vs another cancel
// cas设置前置节点的后节点失败时,很明显我们失败了竞争,
// or signal, so no further action is necessary.
// 这样我们就不用做额外的事情了
Node predNext = pred.next;
// Can use unconditional write instead of CAS here.
// 可以使用无条件写取代cas写
// After this atomic step, other Nodes can skip past us.
// 当这个原子步骤结束后,其他节点就可以跳过我们
// Before, we are free of interference from other threads.
// 在这之前,我们不受其他线程影响;这样cas也不会失败,因为我们没有跳过
node.waitStatus = Node.CANCELLED;
// If we are the tail, remove ourselves.
// 如果当前处于队尾,并cas设置队尾为前置节点;进不入该分支,表明我们已经不是队尾
if (node == tail && compareAndSetTail(node, pred)) {
// 进入分支,无论这一步是否成功,都表示我们不再是队尾,且我们是取消的,而且我们失败,意味着别人已经将前置节点的后即节点更改了,do nothing
compareAndSetNext(pred, predNext, null);
} else {
// If successor needs signal, try to set pred's next-link
// 如果继任者需要唤醒,尝试设置前置节点的下一个节点
// so it will get one. Otherwise wake it up to propagate.
// 因此它会得到一个;另一方面我们唤醒下一个节点,使得传播继续
int ws;
// 第二个分类,当前节点不是尾节点,不是头节点,且前置节点可以唤醒下一个节点的标志
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
// 尝试设置前置节点的下一个节点是当前节点的下一个非空节点
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
// 第三个分类,则是处于头节点,则唤醒下一个节点
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
总结
我们分析了同步队列的创建,获取资源入队,条件对列转移入队,常规出队以及异常取消出队;下一节我们将分析条件队列;