深入理解 JUC:AQS 队列同步器( 九 )

上述方法首先会获取前驱结点的等待状态 , 并依据具体的状态值进行决策:

  1. 如果前驱结点等待状态为 SIGNAL , 则说明当前结点需要被阻塞 , 所以直接返回 true;
  2. 否则 , 如果前驱结点的等待状态大于 0(即处于取消状态) , 则一直往前寻找未被取消的结点 , 并将当前结点排在其后 , 这种情况下直接返回 false , 再次尝试获取一次资源;
  3. 否则 , 前驱结点的状态为 0 或 PROPAGATE(不可能为 CONDITION 状态 , 因为当前处于同步队列) , 因为当前结点需要一个唤醒信号 , 所以修改前驱结点的状态为 SIGNAL , 这种情况下同样返回 false , 以再次确认不能获取到资源 。
如果上述检查返回 true , 则接下来会调用 AbstractQueuedSynchronizer#parkAndCheckInterrupt 方法 , 基于 LockSupport 工具阻塞当前线程 , 并在线程苏醒时检查中断状态 。 如果期间被中断过则记录中断标记 , 而不立即响应 , 直到成功获取到资源 , 或者期间发生异常退出自旋 。 方法 AbstractQueuedSynchronizer#acquireQueued最终会返回这一中断标记 , 并在外围进行响应 。
如果在自旋期间发生异常 , 则上述方法会执行 AbstractQueuedSynchronizer#cancelAcquire 以取消当前结点等待获取资源的进程 , 包括设置结点的等待状态为 CANCELLED , 唤醒后继结点等 。
独占释放资源针对独占模式释放资源 , AbstractQueuedSynchronizer 定义了单一实现 , 即 AbstractQueuedSynchronizer#release 方法 , 该方法本质上是一个调度的过程 , 具体释放资源的操作交由 tryRelease 方法完成 , 由子类实现 。 方法 AbstractQueuedSynchronizer#release 实现如下:
public final boolean release(int arg) {// 尝试释放资源if (this.tryRelease(arg)) {Node h = head;// 如果释放资源成功 , 则尝试唤醒后继结点if (h != null}return true;}return false;}如果 tryRelease 释放资源成功 , 则上述方法会尝试唤醒同步队列中由后往前距离头结点最近的一个结点上的线程 。 方法 AbstractQueuedSynchronizer#unparkSuccessor 的实现如下:
private void unparkSuccessor(Node node) {// 获取当前结点状态int ws = node.waitStatus;if (ws < 0) {// 如果当前结点未被取消 , 则基于 CAS 更新结点等待状态为 0compareAndSetWaitStatus(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; // 获取后继结点// 如果后继结点为 null , 或者被取消if (s == null || s.waitStatus > 0) {s = null;// 从后往前寻找距离当前结点最近的一个未被取消的线程结点for (Node t = tail; t != nullt = t.prev) {if (t.waitStatus <= 0) {s = t;}}}// 唤醒结点上的线程if (s != null) {LockSupport.unpark(s.thread);}}选举待唤醒线程结点的过程被设计成从后往前遍历 , 寻找距离当前结点最近的未被取消的结点 , 并调用 LockSupport 工具类唤醒结点上的线程 。
那 为什么要设计成从后往前遍历同步队列呢 ?在 Doug Lea 大神的论文中给出了答案 , 摘录如下:
An AbstractQueuedSynchronizer queue node contains a next link to its successor. But because there are no applicable techniques for lock-free atomic insertion of double-linked listnodes using compareAndSet, this link is not atomically set as part of insertion; it is simply assigned: pred.next = node; after the insertion. This is reflected in all usages. The next link is treated only as an optimized path. If a node's successor does not appear to exist (or appears to be cancelled) via its next field, it is always possible to start at the tail of the list and traverse backwards using the pred field to accurately check if therereally is one.