前言:
上一篇我们分析了 ReentrantLock 的原理,发现其中的大量实现依赖同步器 AbstractQueuedSynchronizer,也是我们常说的 AQS,AbstractQueuedSynchronizer 到底是个什么神奇的东西,本篇我们将深入分析 AbstractQueuedSynchronizer 源码。
ReentrantLock 传送门:
什么是 AbstractQueuedSynchronizer?
AbstractQueuedSynchronizer(AQS)是 java.util.concurrent.locks目录下的一个类,用来构建锁和同步器的框架,使用 AbstractQueuedSynchronizer 能简单高效的构造出同步器,前文聊到的 ReentrantLock,以及 ReentrantReadWriteLock、Semaphore、CountDownLatch 等都是基于 AbstractQueuedSynchronizer 实现的,我们也可以利用 AbstractQueuedSynchronizer 轻松的实现出符合自己需求的同步器。
AbstractQueuedSynchronizer 的原理:
AbstractQueuedSynchronizer 依赖 state、CAS、CHL 实现了同步。
- state:AbstractQueuedSynchronizer 的一个 int 成员变量,使用 volatile 修饰,保证可见性,标识共享资源的状态,state为 0 标识共享资源没有被占用,state 大于 0 表示共享资源被占用,state 小于 0 则会抛出异常。
- CAS:AbstractQueuedSynchronizer 中使用 CAS 来操作 state 变量,确保 state 的改变是安全的。
- CHL:AbstractQueuedSynchronizer 并没有使用原始 CHL 队列,而是基于原始 CHL 队列做了一些变化,CHL是 AbstractQueuedSynchronizer 实现同步的非常核心的一环,CHL是一种 FIFO(先进先出)的逻辑队列(也有叫虚拟队列),多线程竞争共享资源的时候,没有获取到资源的线程会进入 CHL 队列的队尾排队等待获取锁(每个线程就是 CHL 队列的一个 Node)。
CHL 这种队列据说是由 Craig、Landin 和 Hagersten 三个人一起发明的,因此命名为 CLH 队列。
AbstractQueuedSynchronizer 使用的 CHL 队列基于原始 CHL 队列的一种实现,原始 CHL 队列一般用于自旋锁,而,AbstractQueuedSynchronizer 实现的 CHL 的队列基于原始 CHL 队列先让获取不到共享资源的线程做了一段时间的自旋,然后就让线程挂起,简单来说就是 AbstractQueuedSynchronizer 中的 CHL 队列解决了每个线程无限自旋的问题。
CHL 队列简图:
AbstractQueuedSynchronizer 中 CHL 队列的 Node 源码分析:
static final class Node {
//共享节点的标识
static final Node SHARED = new Node();
//独占节点的标识
static final Node EXCLUSIVE = null;
//节点状态为取消状态 取消状态的节点只能等待被队列移除
static final int CANCELLED = 1;
//节点为 SIGNAL 状态 则其后驱节点等待被唤醒 也可以理解为当前节点将要释放锁 需要唤醒后续节点
static final int SIGNAL = -1;
//条件节点 不会位于同步节点中
static final int CONDITION = -2;
//共享模式才有的状态
static final int PROPAGATE = -3;
//节点的等待状态 默认0 代表初始化状态
volatile int waitStatus;
//前驱节点
volatile Node prev;
//后驱节点
volatile Node next;
//当前节点的线程
volatile Thread thread;
//下一个节点
Node nextWaiter;
//是否是共享节点是就返回 true
final boolean isShared() {
return nextWaiter == SHARED;
}
//返回前一个节点
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
//构造方法
Node() { // Used to establish initial head or SHARED marker
}
//构造方法 addWaiter 方法使用
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
//构造方法 Condition 条件是时候使用
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
注意:AbstractQueuedSynchronizer 队列的头节点 node 是虚拟节点,后面会解答为什么是虚拟节点。
AbstractQueuedSynchronizer 核心方法分析:
acquire 方法分析:
在 ReentrantLock 篇章中我们提到了 acquire 方法是加锁的核心方法,同时也是 AbstractQueuedSynchronizer 的核心方法,下面我们来分析一下 acquire 方法。
acquire 方法调用链路分析:
acquire 方法源码分析:
//获取锁的方法
public final void acquire(int arg) {
//这里分为三个方法 tryAcquire 、acquireQueued、addWaiter
//tryAcquire 先尝试直接获取锁 看是否能够成功获取
//addWaiter 方法是把当前线程包装成一个 node 节点加入到 CHL 队列 是acquireQueued addWaiter方法的返回值是 acquireQueued 方法的一个参数
//tryAcquire 不能成功获取锁后 调用acquireQueued 获取锁 这个方法一定能够成功获取锁(程序不出异常的情况)
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquire 方法是一个极其简单的方法,主要是包装了 tryAcquire 、acquireQueued、addWaiter、 selfInterrupt 四个方法,接下来我们逐步分析这个三个方法。
tryAcquire 方法这里不做分析了,我们知道 tryAcquire 方法实际调用的是 NonfairSync(非公平锁) 或者 FairSync(公平锁) 的 tryAcquire 方法,在 ReentrantLock 篇章中已经详细分析过,ReentrantLock 源码分析传送门如下:
addWaiter 源码分析:
//为当前线程创建独占节点或者共享节点并将其放到队列尾部 Node.EXCLUSIVE 为独占节点 Node.SHARED 为共享 节点
private Node addWaiter(Node mode) {
//为当前线程创建一个新的节点
//mode 有两个值 分别代表两种不同模式 Node.EXCLUSIVE 为独占节点 Node.SHARED 为共享 节点
Node node = new Node(Thread.currentThread(), mode);
//获取队列尾部节点 我理解为临时存储
Node pred = tail;
//判断尾部节点是否为空
if (pred != null) {
//尾部节点不为空 将刚刚创建的节点的前驱节点指向尾节点
node.prev = pred;
//使用 CAS 用设置刚刚创建的节点为尾节点
if (compareAndSetTail(pred, node)) {
//设置成功后将刚刚创建的节点作为之前的尾节点的后驱节点
pred.next = node;
return node;
}
}
//尾节点为空 说明队列没有创建 需要创建一个空的头节点 并把刚刚创建的节点作为队列尾节点
enq(node);
return node;
}
//初始化队列 并把node 节点加入队列作为队列的尾节点
private Node enq(final Node node) {
//自旋
for (;;) {
//把尾节点 赋值给 t
Node t = tail;
//判断尾节点是否为空
if (t == null) { // Must initialize
//尾节点为空 创建一个新的空节点 并使用 CAS 将创建的空节点设置为队列头节点
if (compareAndSetHead(new Node()))
//头节点设置成功后 也就是队列初始化成功后 把队列的尾节点也指向刚刚创建的空的头节点
tail = head;
} else {
//尾节点不为空 表示队列初始化完成了 至少是第二次循环了 将node节点的前驱指针指向当前队列的尾节点
node.prev = t;
//使用 CAS 用设置node 节点为新的尾节点
if (compareAndSetTail(t, node)) {
//设置成功后 将之前的尾节点 t 的后驱指针指向 node 节点
t.next = node;
//node 节点成功加入到队列尾部
return t;
}
}
}
}
通过对 addWaiter 方法的源码分析,我们知道该方法其实就是把当前线程封装成一个 Node 节点加入队列。
acquireQueued 源码分析:
//获取共享资源 即锁
final boolean acquireQueued(final Node node, int arg) {
//是否获取成功的标识
boolean failed = true;
try {
//线程是否中断的标识
boolean interrupted = false;
//自旋获取锁
for (;;) {
//获取当前节点的前一个节点 Node 源码中我们分析过
final Node p = node.predecessor();
//判断当前节点的前一个节点是否是头节点 如果是 再次调用 tryAcquire 方法尝试获取锁
// tryAcquire 方法实际调用的是 NonfairSync(非公平锁) 或者 FairSync(公平锁) 的 tryAcquire 方法 之前有分析过 这里不做分析
if (p == head && tryAcquire(arg)) {
//当前节点的前一个节点是头节点 且当前节点 tryAcquire 获取锁成功 则设置队列头节点为当前节点
setHead(node);
//因为当前节点已经是头节点了 那之前节点要从队列中移除了 设置之前的头节点的下一个节点为空 方便JVM 进行GC 操作
p.next = null; // help GC
//设置获取成功标志位flase 这里设置 false 是因为线程没有打断 无需执行 acquire 方法中的 selfInterrupt 方法
failed = false;
//返回打断标志位false
return interrupted;
}
//当前节点的前一个节点不是头节点 或者 tryAcquire 获取锁失败 需要判断当前节点是否需要被阻塞 防止一直进行自旋 浪费CPU 资源
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//能进入这里 表示线程被打断过 设置打断标识为 true
interrupted = true;
}
} finally {
if (failed)
//程序异常 才会走到这里 cancelAcquire 方法就是把 node 状态标记为取消状态
cancelAcquire(node);
}
}
acquireQueued 方法自旋获取锁,但是有不是一直进行自旋,必要的時候会把线程挂起,会根据 shouldParkAfterFailedAcquire 方法的结果那判断是否要继续自旋还是挂起线程,如果需要挂起线程则执行 parkAndCheckInterrupt 。
shouldParkAfterFailedAcquire 源码分析:
//根据当前节点的前驱节点 判断当前节点是否应该被阻塞
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//当前节点的前驱节点的状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 根据 Node 源码中的状态解析 我们知道当前节点的前驱节点处于唤醒状态
return true;
if (ws > 0) {
// 根据 Node 源码中的状态解析 我们知道 前驱节点是取消状态 我们需要在队列中移除它 并循环移除它前面的节点 并找到有效的前驱节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//当前节点的前驱节点既不是唤醒状态 也不是取消节点 那就 CAS 设置当前节点的前驱节点状态为 Node.SIGNAL 阻塞状态
//此时前驱节点的状态 只能是0 或者是 PROPAGATE -3
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
//不阻塞
return false;
}
通过分析 shouldParkAfterFailedAcquire 源码,发现当前节点的前驱节点既不是唤醒状态也不是取消节点,为什么不直接返回 true,结束自旋,而是返回 false,机选进行下一次循环呢?
这里我们分析一下,前驱节点 waitStatus 不是 Node.SIGNAL 状态,也不是取消状态,那前驱节点的状态只能为 0 或者 PROPAGATE时,waitStatus 为 0 只能是正在释放锁的节点或者是新节点,如果是正在释放锁的节点,那我们再次循环一次就可以拿到锁了,如果是新节点我们再循环一次也可以正常阻塞,如果是 waitStatus 是 PROPAGATE 状态,只存在共享状态下,且只有头节点才有这个状态,同样我们再次循环一次就可以获取到锁,多循环一次的开销远小于线程阻塞唤醒的开销,所以这里才返回 false,不让线程挂起。
cancelAcquire 方法源码分析:
//设置节点状态为取消节点
private void cancelAcquire(Node node) {
//为空判断
if (node == null)
return;
//设置当前节点的所属线程为空
node.thread = null;
//获取当前节点的前驱节点
Node pred = node.prev;
//前驱节点状态大于0 表示取消节点 一直找到有效的前驱节点
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
//获取当前节点的 前驱节点的 后驱节点
Node predNext = pred.next;
//设置当前节点的状态为 取消状态
node.waitStatus = Node.CANCELLED;
if (node == tail && compareAndSetTail(node, pred)) {
//如果当前节点是尾节点 且 CAS 操作将当前节点的前驱节点设置为 尾节点成功 则 CAS 操作将当前节点的前驱节点的后驱节点设置为 null
compareAndSetNext(pred, predNext, null);
} else {
//如果当前节点是尾节点 且 CAS 操作将当前节点的前驱节点设置为 尾节点失败
//节点状态
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
//当前节点的前驱节点不是头节点
//1 当前节点的前驱节点状态为阻塞
//2 当前节点的前驱节点状态小于0 且将前驱节点的状态设置为 Node.SIGNAL
//1 2 有一个为 true 在判断当前节点的前驱节点线程是否为空
//获取当前节点的后驱节点
Node next = node.next;
if (next != null && next.waitStatus <= 0)
//当前节点的后驱节点不为空 且状态小于0
//CAS 操作把当前节点的前驱节点的后驱节点设置为当前节点的后驱节点
compareAndSetNext(pred, predNext, next);
} else {
//如果当前节点就是head 节点的后驱节点 或者不满足上述条件 就唤醒当前节点的后面的节点
unparkSuccessor(node);
}
//被设置为取消的节点的后驱节点指向自己 方便JVM内存回收
node.next = node; // help GC
}
}
通过我们对 cancelAcquire 方法源码的分析,知道 cancelAcquire 的作用就是把传入的节点设置为 CANCELLED 状态,并通过 unparkSuccessor 方法唤醒后续节点。
unparkSuccessor 方法源码分析:
//唤醒当前节点后面的节点
private void unparkSuccessor(Node node) {
//获取当前节点的状态
int ws = node.waitStatus;
if (ws < 0)
//小于0 设置当前节点的状态为 0
compareAndSetWaitStatus(node, ws, 0);
//当前节点的后驱节点
Node s = node.next;
if (s == null || s.waitStatus > 0) {
//当前节点的后驱节点为空 或者 后驱节点状态大于 0 其实就是取消状态 无效节点
s = null;
//从队列尾部开始遍历 直到找到最近的一个有效节点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
//状态小于 0 表示是有效的节点
s = t;
}
if (s != null)
//唤醒找到的后驱节点
LockSupport.unpark(s.thread);
}
unparkSuccessor 方法就是找到当前节点的下一个有效节点并唤醒它。
parkAndCheckInterrupt 方法源码解析:
//挂起当前线程 返回线程中断状态
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
parkAndCheckInterrupt 方法逻辑很简单,就是挂起当前线程,返回挂起状态。
selfInterrupt 方法源码解析:
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
线程在等待资源的过程中被唤醒,唤醒后还是会不断地去尝试获取锁,直到抢到锁为止,在整个流程中,并不响应中断,只是记录中断记录,最后抢到锁返回了,那么如果被中断过的话,就需要利用 selfInterrupt 方法补充一次中断,让调用者线程感知到被中断过。
AbstractQueuedSynchronizer 的解锁方法 release 源码分析:
在 ReentrantLock 篇章中我们说不管公平锁还是非公平锁最终调用的解锁方法都是 AbstractQueuedSynchronizer 的 release 方法来实现的,我们来分析一下 release 方法。
//独占模式解锁
public final boolean release(int arg) {
//tryRelease 调用的是 ReentrantLock 的 tryRelease 方法 将 state 设置为0 并设置当前线程不占有锁 在 ReentrantLock 篇章中有分析过
if (tryRelease(arg)) {
//tryRelease 返回true 表示解锁成功 且锁没有被其他线程占有
Node h = head;
if (h != null && h.waitStatus != 0)
//头节点不为空 且状态不是0 0表示初始化节点 唤醒头节点后面的节点 unparkSuccessor 方法上面分析过
unparkSuccessor(h);
return true;
}
return false;
}
release 方法一共做了两个操作如下:
- 调用 ReentrantLock 的 tryRelease 方法 将 state 设置为 0,并设置当前线程不再占用锁
- 调用 unparkSuccessor 方法唤醒后面的有效节点。
AbstractQueuedSynchronizer 的队列的头节点为虚拟节点,为什么要虚拟这个节点?
我们知道 Node 节点中有一个 waitStatus 变量,来标识节点的状态,通过上面的源码分析我们知道这其中有一个很重要的状态 SIGNAL ,该状态标识着我们是否需要需要唤醒下一个节点,也就是说每个节点在挂起休眠前都需要将他的前置节点状态标记为 SIGNAL 状态,这样就必须要要有一个前置节点,前置节点其实就是持有锁的节点,释放锁之后需要唤醒下一个节点,那第一个节点的前置节点是谁?最终采用的方式就是创建一个节点,也就是虚拟一个节点。
如有错误的地方欢迎指出纠正。