阅读须知
- JDK版本:1.8
- 文章中使用/* */注释的方法会做深入分析
正文
随着 jdk1.5 版本的发布,Doug Lea 大神编写的 util.concurrent 包正式与我们相见,里面很多的同步器如 ReentrantLock(重入锁)、ReadWriteLock(读写锁)、CountDownLatch(闭锁)、Semaphore(信号量)还有线程池等都是基于AQS(AbstractQueuedSynchronizer)构建的。
在分析 AQS 之前,我们首先需要了解一下 LockSupport 这个工具类,AQS 内部会用到这个工具类:
LockSupport 是创建锁和其他同步类的基础。
LockSupport 与使用它的每个线程关联一个许可。如果有许可,将立即返回对 park 方法的调用,并在此过程中消耗掉这个许可;否则它可能会阻塞。 调用 unpark 方法可使许可可用(如果尚不可用)。许可不会累积。最多只能有一个。
方法 park 和 unpark 提供了有效的阻塞和解除阻塞线程的方法,这些线程不会遇到导致已弃用的方法 Thread.suspend 和 Thread.resume 无法用于以下目的的问题:基于许可,调用 park 的线程与试图进行 unpark 的另一个线程竞争将保持存活。此外,如果调用者的线程被中断并且支持超时版本,则 park 将返回。park 方法也可能在其他任何时间出于“无理由”返回,因此通常必须在循环中调用,该循环在返回时会重新检查条件。从这个意义上说,park 是对“busy wait”的优化,它不会浪费太多的时间,但是必须与 unpark 配对才能生效。
通过 LockSupport 介绍,我们可以获取到两个比较有用的信息,第一,我们可以使用 LockSupport 对线程做阻塞和解除阻塞操作;第二,LockSupport.park 方法会在线程中断和超时时返回。后面在分析 AQS 源码时,我们会看到 LockSupport 的使用。
AQS 是一个非常精妙的框架,我们先来看一下 AbstractQueuedSynchronizer 类注释的介绍(个人翻译):
提供了一个框架,用于实现依赖于先进先出(FIFO)等待队列的阻塞锁和相关同步器(信号量,事件等)。这个类被设计为依赖于单个原子 int 值来表示状态,是大多数同步器的基础。子类必须定义改变此状态的 protected 方法,并且定义该状态对于获取或释放此对象的含义。鉴于这些,类中的其他方法将执行所有排队和阻塞机制。子类可以维护其他状态字段,但是只有使用方法 getState,setState 和 compareAndSetState 操纵的原子更新的 int 值才会跟踪同步。
在这段注释中,有两个比较重要的信息,第一,AQS 使用一个 int 型的字段来表示同步状态,第二,AQS 依赖 FIFO(first-in-first-out 先进先出)等待队列来完成线程的排队工作。
首先了解一下 AQS 的成员变量:
AbstractQueuedSynchronizer:
// 等待队列的头(头结点),延迟初始化。除了初始化外,它只能通过 setHead 方法修改。注意:如果头存在,它的 waitStatus 保证不能是 CANCELLED
private transient volatile Node head;
// 等待队列的尾(尾节点),延迟初始化。仅能通过 enq 方法修改以添加新的等待节点
private transient volatile Node tail;
// 同步状态
private volatile int state;
这里队列的头尾节点和同步状态都用 volatile 修饰,保证了多线程之间的可见性。
节点类的实现:
AbstractQueuedSynchronizer.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 值表明线程正在在条件上等待
static final int CONDITION = -2;
// waitStatus 值表明下一个 acquireShared 应无条件传播,PROPAGATE 只用在共享模式下
static final int PROPAGATE = -3;
// 状态字段,仅能为以上几个状态值或者为0(以上都不是,初始化状态)
volatile int waitStatus;
// 当前节点的前任节点,用于检查 waitStatus,排队期间分配,并仅在出队时清空(为了 GC 的缘故)
volatile Node prev;
// 当前节点的后继节点,在当前节点发布后唤醒,排队期间分配,绕过取消的前置节点(代表所有在当前节点之前的节点)进行调整,并在出队时清空(出于 GC 的原因)
volatile Node next;
// 排入此节点的线程。在构造时初始化,在使用后清空
volatile Thread thread;
// 等待条件队列中的后继节点,或特殊值 SHARED
Node nextWaiter;
这里给出节点之间的关系图:
我们以 acquire 和 release 两个方法来分析获取锁和释放锁的过程,首先我们尝试阅读一下 acquire 方法的注释:
以独占模式获取,忽略中断。通过调用至少一次 tryAcquire 来实现,并在成功时返回。否则,线程排队,可能重复阻塞和解除阻塞,调用 tryAcquire 直到成功为止。此方法可用于实现 Lock 接口的 lock 方法
AbstractQueuedSynchronizer:
public final void acquire(int arg) {
// tryAcquire 方法尝试以独占模式获取锁,该方法应该查询对象的 state 是否允许在独占模式下获取锁,如果是,则获取它
/* addWaiter:为当前线程和给定模式创建并排队节点 */
/* acquireQueued:为队列中的线程获取独占不中断模式。由条件等待方法使用以及获取 */
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 如果当前线程获取锁失败并且排队失败,则中断当前线程
selfInterrupt();
}
AbstractQueuedSynchronizer:
private Node addWaiter(Node mode) {
// 使用当前线程和给定模式作为参数构建 Node 节点,acquire 方法传入的是 Node.EXCLUSIVE 也就是独占模式
Node node = new Node(Thread.currentThread(), mode);
// 这里首先会尝试快速的入队方式,如果不成功,后面会进入普通的入队方式
Node pred = tail;
// 判断尾节点是否为 null
if (pred != null) {
// 如果尾节点不为 null,则将当前节点的前任节点指向尾节点
node.prev = pred;
// CAS 尝试将当前节点设置为尾节点,这里可能有多个线程都在尝试此操作,只有一个会成功
if (compareAndSetTail(pred, node)) {
// 如果 CAS 成功,则将尾节点的后继节点设置为当前节点并返回当前节点
pred.next = node;
return node;
}
}
/* 将节点插入队列,如有必要进行初始化 */
enq(node);
return node;
}
AbstractQueuedSynchronizer:
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 判断尾节点是否为 null
if (t == null) {
// 这是一个初始化操作,如果尾节点 null,CAS 建立初始头节点,是一个空节点
if (compareAndSetHead(new Node()))
// CAS 操作成功后将头结点赋值给尾节点,也就是说,初始化状态,头结点和尾节点都指向一个空节点
tail = head;
} else {
// 如果尾节点不为 null,则将当前节点的前任节点指向尾节点
node.prev = t;
// CAS 尝试将当前节点设置为尾节点,这里在自旋中完成 CAS,所以最终一定会成功
if (compareAndSetTail(t, node)) {
// CAS 操作成功后将尾节点的后继节点指向当前节点
t.next = node;
return t;
}
}
}
}
我们分析一下这个方法的流程:
- 默认情况下 head = null, tail = null
- loop1: head = tail = 空节点,在第一次循环之后,头尾节点都指向了一个空节点
- loop2: head = 空节点, tail = 当前节点, 当前节点的 prev 指向 head,head 的 next 指向当前节点,这样就形成了一个双向队列
入队操作可能存在多线程竞争的情况,所以入队操作需要在自旋中进行,保证最后一定能够入队成功
不知道大家是否有疑问,addWaiter 方法中已经有一个“将当前节点的前任节点指向尾节点“操作,这里为什么还要再次执行重复的操作呢?因为 addWaiter 方法将当前节点设置为尾节点的 CAS 操作可能会失败,如果失败了,addWaiter 方法中的这个操作就是有误的,会存在多个节点的前任节点指向同一个节点的情况,到这里尾节点可能已经变了,所以这里需要重新执行一遍这个操作。
AbstractQueuedSynchronizer:
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);
// 帮助 GC
p.next = null;
// 标记再次获取锁成功
failed = false;
// 返回中断状态
return interrupted;
}
/* 判断再次获取锁失败后是否应该阻塞,如果需要阻塞,则在阻塞后判断线程的中断状态 */
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 如果已经被中断,将中断标记设置为 true
interrupted = true;
}
} finally {
if (failed)
/* 如果获取锁失败,取消正在进行的获取尝试 */
cancelAcquire(node);
}
}
当前节点插入到队尾后,线程不会立马阻塞,会进行自旋操作。因为在当前节点入队到阻塞的过程中,当前持有锁的线程(头节点的线程)可能已经执行完成,所以要判断该当前节点的前任节点是否为头节点,如果是,表明当前节点是队列中第一个应该获取锁的节点,因此再次尝试 tryAcquire 获取锁,如果成功获取到锁,表明之前持有锁的线程已经执行完成,当前线程不需要挂起。如果获取失败,表示之前持有锁的线程还未完成,至少还未修改 state 值,这时就要判断是否应该阻塞:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 如果当前节点的前任节点的状态为 SIGNAL,则当前节点可以安全的阻塞
return true;
if (ws > 0) {
// 如果当前节点的前任节点的状态为 CANCELLED,也就是已经是取消状态
// 这时需要跳过前任节点循环进行重试(再次进行这个判断),目的是为了跳过相邻的所有的 CANCELLED 状态的前任节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
// 找到了最近的状态不为 CANCELLED 的前置节点后,将这个节点的后继节点设置为当前节点
pred.next = node;
} else {
// 到这里当前节点的前任节点的状态是0或 PROPAGATE。表明我们需要一个信号,但不要阻塞。需要重试以确保在阻塞前无法获取锁。CAS 将当前节点的前任节点的状态设置为 SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
这里涉及到节点状态的判断,上文我们介绍过节点相关的几个状态的含义,这里我们通过注释进行详细说明便于更好的理解:
SIGNAL:该节点的后继节点(或将很快)被阻塞(通过 park),因此当前节点在释放或取消时必须唤醒其后继节点。为了避免竞争,获取方法必须首先表明他们需要一个信号,然后重试原子获取,然后在失败时阻塞
CANCELLED:由于超时或中断,此节点被取消。节点永远不会离开这个状态。特别是,具有取消状态的节点线程不会再次阻塞
CONDITION:节点当前处于条件队列中。它不会被用作同步队列节点,直到传输完成,此时状态将被设置为0(此处使用此值与该字段的其他用途无关,但会简化机制)
PROPAGATE:releaseShared 操作应该传播到其他节点。在 doReleaseShared 方法中设置此状态(仅作用于头节点)以确保传播继续,即使其他操作已经干预也是如此
我们用上文分析 enq 方法的例子来分析下这个方法的流程:
- 在当前节点成功入队后,头结点为初始化状态为0的空间点,当前节点的 prev 指向头结点并且状态也为0。
- 这个时候程序进入这个方法后会进入到最后的 else 分支里面,将头节点的状态设置为 SIGNAL。
- 方法执行结束返回 false,这时 acquireQueued 方法的自旋操作会进入第二次循环继续尝试获取锁。
- 如果锁还是没有获取成功,再次进入 shouldParkAfterFailedAcquire 方法后会返回 true,这时就会进入 parkAndCheckInterrupt 进行阻塞操作。
AbstractQueuedSynchronizer:
private final boolean parkAndCheckInterrupt() {
// 阻塞当前线程
LockSupport.park(this);
// 在当前线程从阻塞状态被唤醒后,返回当前线程的中断状态
return Thread.interrupted();
}
这里线程进入到阻塞状态之后,有几种唤醒的方式,第一当然就是当前持有锁的线程释放锁之后,会唤醒队列中下一个应该获取锁的节点;其次就是线程被中断或者超时,park 将返回,LockSupport.park 方法的这个特性我们上面提到过。
这里额外提一下,AQS 还支持另外的方式处理线程中断和超时,我们会发现 AQS 中有几个以 Interruptibly 作为方法名后缀的方法,例如我们现在分析的过程是以 acquire 方法作为入口,AQS 中还有一个 acquireInterruptibly 方法,这两个方法不同的地方就是对线程中断的处理方式不同,acquire 方法在线程中断时只是传递线程中断状态,而 acquireInterruptibly 在线程中断时会抛出 InterruptedException,所以我们通常把以 Interruptibly 作为方法名后缀的方法称为响应中断的方法。
另外 AQS 也有几个支持传入超时时间的方法,如 tryAcquireNanos,会在给定的时间范围内尝试获取锁,如果在给定的时间范围内没有获取到锁,则会返回 false 代表锁获取失败。上述两种情况发生后,则会进入下面的 cancelAcquire 方法取消当前节点对锁的获取:
AbstractQueuedSynchronizer:
private void cancelAcquire(Node node) {
if (node == null)
return;
node.thread = null;
// 跳过已经取消的前置节点,找到最近的状态不为 CANCELLED 的前置节点
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
Node predNext = pred.next;
// 可以在此处使用无条件写入代替 CAS。完成这一原子操作后,其他节点可以跳过当前节点。在这之前,当前节点不受其他线程的干扰
node.waitStatus = Node.CANCELLED;
// 如果当前节点是尾节点,尝试 CAS 将当前节点的前任节点设置为尾节点
if (node == tail && compareAndSetTail(node, pred)) {
// 如果 CAS 操作成功,尝试 CAS 将当前节点的前任节点的后继节点设置为 null,这样一来就断开了当前节点的前任节点与它的所有后继节点
// 这些节点由于变得不可达,最终会被回收掉,这里的 CAS 更操作失败了也没关系,说明有其它新入队线程或者其它取消线程更新掉了
compareAndSetNext(pred, predNext, null);
} else {
// 进入这里,说明当前节点后面还有节点,这时需要将当前节点的前任节点和后面的非取消状态的节点拼接起来
int ws;
// 判断当前节点的前任节点不是头结点(并且状态为 SIGNAL 或者(非取消状态并且 CAS 可以将状态设置为 SIGNAL))并且线程不为null
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)
// 如果当前节点的后继节点为非取消状态的话,则 CAS 尝试把当前节点的前任节点的后继节点置为当前节点的后继节点,这样就连接了起来
// 这里 if 条件为 false 或者 CAS 失败都没关系,这说明可能有多个线程在取消,总归会有一个能成功的
compareAndSetNext(pred, predNext, next);
} else {
/* 如果上面的 if 条件不满足,尝试唤醒后继节点 */
unparkSuccessor(node);
}
// 帮助 GC
node.next = node;
}
}
这里当前节点的后继节点之所以设置为自己本身而不是 null,是为了方便 AQS 中 Condition 部分的 isOnSyncQueue 方法,判断一个原先属于条件队列的节点是否转移到了同步队列。因为同步队列中会用到节点的 next 域,取消节点的 next 也有值的话,可以断言 next 与有值的节点一定在同步队列上。在 GC 层面,和设置为 null 具有相同的效果。
AbstractQueuedSynchronizer:
private void unparkSuccessor(Node node) {
// 如果状态为负值(即可能需要 signal),尝试清除状态。如果失败或状态被等待线程改变,则 OK
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
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);
}
独占锁获取的过程到这里就分析完了,下面我们来看释放锁的过程,首先还是尝试阅读 release 方法的注释:
以独占模式释放。如果 tryRelease 返回 true,则通过解锁一个或多个线程来实现。该方法可用于实现 Lock 接口的 unlock 方法
AbstractQueuedSynchronizer:
public final boolean release(int arg) {
// tryRelease 方法主要用于子类重写通过 CAS 指令修改状态变量 state 尝试释放锁
if (tryRelease(arg)) {
Node h = head;
// head 节点状态不会是 CANCELLED,所以这里相当于判断状态小于0
if (h != null && h.waitStatus != 0)
// 唤醒后继节点的线程,上文已经分析过
unparkSuccessor(h);
return true;
}
return false;
}
到这里,整个 AQS 独占锁的源码解析就完成了。