闲话
看ThreadPoolExecutor 源码的时候,其中 Worker 类是基于 AbstractQueuedSynchronizer 构建的,所以顺便把这个类一起看了。另外,这个类也是ReenTrantLock 和 Semaphore 的底层的机制,说明足够重要了。为了方便,下文统一使用AQS 指代 AbstractQueuedSynchronizer 。
一、简单了解
1.1、AQS —为了同步而生
AQS 由 Doug Lea 大神编写,主要目的是提供同步功能。AQS 内部维护了一个 int 类型的变量 state。这个 state 的不同值,可以代表不同的状态,AQS 操作 state 变量时,使用 CAS,保证了原子性(多线程环境下安全)。通过在多线程环境下,对 state 不同的值代表的不同状态进行赋予不同的含义,AQS 就可以当做一个比较完备的同步器来使用。这里,对”状态“这个词做下解释,举两个例子。第一个例子是互斥锁:state 为0,代表互斥锁被占用,state 为1,代表互斥锁可用。第二个例子是信号量(对共享资源的访问):state 为10,代表当前有10个资源(例如数据库连接)可用,state 为5,代表当前有 5个资源(数据库连接)可用,state <= 0,则代表当前没有资源可用,必须等待其他资源释放后,才能使用。除了上述两个场景外,其他同步场景,AQS 都可能作为一个比较完美的方案来使用。
AQS 是一个抽象类,并提供了 互斥 和 共享 两种同步场景。但是由于,AQS 的核心在于维护 state 变量,至于怎么通过 state 来实现 互斥 或者 共享,AQS 并没有过多干涉,只是提供了一些空实现的方法,来让子类具体实现。而且,AQS 提供了对条件变量的支持,可以说是很通用了。
我对与 AQS 的理解目前只到这一步,所以我认为,AQS 比较重要的部分是,如何实现对状态变量的操作和维护,以及中间的一些场景,例如,互斥 和 共享场景是如何实现的;如何支持的条件变量。这些点,也是下文要着重关注的一些点。
1.2、AQS 的继承关系
AQS 继承了 AbstractOwnableSynchronizer 类,AbstractOwnableSynchronizer 这个类提供了互斥的语义,比较简单,这里不做赘述。
二、AQS 的机制和源码
2.1、AQS 类的结构
2.1.1、AQS 中 CLH 队列节点结构
AQS 使用了 CLH 自旋锁,去解决 互斥 和 共享 场景下的等待问题。
CLH CLH(Craig, Landin, and Hagersten locks): 是一个自旋锁,能确保无饥饿性,提供先来先服务的公平性。
CLH锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。
节点的四个状态:
- a. CANCELLED = 1:因为超时或者中断,结点会被设置为取消状态,被取消状态的结点不应该去竞争锁,只能保持取消状态不变,不能转换为其他状态。处于这种状态的结点会被踢出队列,被GC回收;
- b. SIGNAL = -1:表示这个结点的继任结点被阻塞了,到时需要通知它;
- c. CONDITION = -2:表示这个结点在条件队列中,因为等待某个条件而被阻塞;
- d. PROPAGATE = -3:使用在共享模式头结点有可能处于这种状态,表示锁的下一次获取可以无条件传播;
- e. 0: None of the above,新结点会处于这种状态。
Node 类的结构:
static final class Node {
/** 共享模式 */
static final Node SHARED = new Node();
/** 独占模式 */
static final Node EXCLUSIVE = null;
/** 状态位:表示线程已经取消的状态值 */
static final int CANCELLED = 1;
/** 状态位:表示后一个节点的线程需要唤醒的状态值 */
static final int SIGNAL = -1;
/** 状态位:线程(处在Condition休眠状态)在等待Condition唤醒 */
static final int CONDITION = -2;
/**
* 使用在共享模式头结点有可能牌处于这种状态,表示锁的下一次获取可以无条件传播;
*/
static final int PROPAGATE = -3;
/**
* CLH 节点的状态位
*/
volatile int waitStatus;
/**
* 前驱结点
*/
volatile Node prev;
/**
* 后置节点
*/
volatile Node next;
/**
* 当前线程
*/
volatile Thread thread;
// 下一个等待条件(Condition)的节点,由于Condition是独占模式,因此这里有一个简单的队列来描述Condition上的线程节点。
Node nextWaiter;
/**
* 返回是否是共享模式
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
* 返回前一个节点,如果为空则抛出NullPointerException。当前任不能为空时使用。可以省略null检查,但它是用来帮助VM的。
*/
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() {
}
// addWaiter 使用
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
// Condition 使用
Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
}
2.1.2、AQS 的成员变量
// 等待队列的头结点
private transient volatile Node head;
// 等待队列的尾结点
private transient volatile Node tail;
// 状态
private volatile int state;
AQS 有3个比较重要的成员变量。
- head CLH 队列的头结点
- tail CLH 队列的尾结点
- AQS 的状态变量
这3个变量都使用了 volatile 修饰,volatile 可以保证有序性、可见性,但不能保证原子性。为了在多线程下,安全的使用这些变量,AQS 使用了 CAS 来操作这些变量。
2.2、互斥场景的实现
2.2.1、AQS 互斥场景下的相关方法
- acquire(int arg) 申请获取状态(无法中断)
- acquireInterruptibly(int arg) 申请获取状态(支持中断)
- release(int arg) 释放状态
先附一张找到的流程图,帮助理解代码
下面我们着重看下 acquire(int arg) 和 release(int arg) 这两个方法
2.2.2、acquire(int arg)
acquire(int arg) 源码
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
// 生成节点,并加入队列
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 由于 acquireQueued 方法在处理中断的时候,会清空中断状态【parkAndCheckInterrupt 方法】,所以,如果 node 在 acquireQueued 中被阻塞的话,这里需要重新设置阻塞状态
selfInterrupt();
}
其中主要有tryAcquire(arg) 、acquireQueued(addWaiter(Node.EXCLUSIVE),arg)、selfInterrupt() 三个操作,下面挨个看下 ,首先是 tryAcquire(arg)
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
很明显这个方法需要由子类自己去实现,实际上,AQS 状态的变化也是在这个方法中完成的,明显的模板方法模式。下面继续看 addWaiter(Node.EXCLUSIVE) :
private Node addWaiter(Node mode) {
// 生成节点
Node node = new Node(Thread.currentThread(), mode);
// 获取当前的尾结点
Node pred = tail;
// 如果尾结点不为 null
if (pred != null) {
// 将新节点的前驱引用指向这个尾结点
node.prev = pred;
// CAS 将新节点设置为尾结点
if (compareAndSetTail(pred, node)) {
// 将原来尾结点的 next 指针指向 新节点
pred.next = node;
return node;
}
}
// 执行到这里,说明要么是 尾结点为 null,要么是 CAS 设置尾结点的时候失败了
enq(node);
return node;
}
下面看下 enq(node) 这个方法执行了:
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
// 如果尾结点 为 null ,那么首先要初始化 head 节点,然后将 tail 节点 指向 head,然后继续循环
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 如果尾结点不为 null 了
// 将新节点的 前驱结点 指向 尾结点
node.prev = t;
// 设置新节点为 尾结点
if (compareAndSetTail(t, node)) {
// 将尾结点的 next 指针指向 新节点
t.next = node;
return t;
}
}
}
}
这样,一个完整的 addWaiter(Node.EXCLUSIVE) 流程就走完了,主要涉及到节点的创建,以及head、tail节点的初始化以及新节点的入队,并不是很麻烦。这个时候,我们回到 acquire(int arg) 方法,继续看 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 。这个方法其实就是让每个节点通过休眠,等待被前置节点唤醒,获取锁。
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)) {
// 如果当前节点的前驱节点是 head 节点,并且成功申请了状态,将进行以下操作:
// 将当前节点设置为头节点
setHead(node);
// 释放 next 指针,帮助 GC
p.next = null;
// 修改标记位
failed = false;
// 返回中断标记
return interrupted;
}
// 判断当前节点是否需要阻塞,如果需要,则进行阻塞。否则继续循环。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 如果 判断当前节点需要阻塞 && 阻塞线程后,判断该线程被中断 ,则将中断标记改为 true
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
比较重要的是 shouldParkAfterFailedAcquire 和 parkAndCheckInterrupt 这两个方法,下面一起看下:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获取 前置节点的状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* 如果状态是 SIGNAL ,则说明需要前置节点 pred 来 唤醒 node,直接休眠
*/
return true;
if (ws > 0) {
/*
*ws >0 说明是 CANCELLED 状态,则一直往前找,直到找到一个节点,而且这个节点的ws 非 CANCELLED 状态的
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
// 舍弃中间的节点
pred.next = node;
} else {
/*
* waitStatus 一定是 0 或者 PROPAGATE。 这个时候,将前置节点的 ws 设置为 SIGNAL,等到下一次再进入shouldParkAfterFailedAcquire 方法的时候,就可以走 ws == Node.SIGNAL 这个分支了
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
// 阻塞当前线程
LockSupport.park(this);
// 注意:这里获取中断状态后,会重新清空中断状态
return Thread.interrupted();
}
shouldParkAfterFailedAcquire 方法,其实就是根据 node 的前置节点的 waitStatus 状态位,来判断是否需要休眠,当 waitStatus 为 SIGNAL 时,线程将被休眠。这里其实就是 AQS 对 CLH 锁进行的变种,即后置节点并不会自旋,而是进行休眠,等可以申请状态的时候,由前置节点进行唤醒。parkAndCheckInterrupt 方法就很简单了,直接使用 LockSupport.park,将当前线程挂起。
2.2.3、release(int arg)
release 源码:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
主要关注这几个方法:
- tryRelease 由子类实现,在这个方法里修改 state
- unparkSuccessor 唤醒后置节点
由于 tryRelease 需要子类实现,我们主要看下 unparkSuccessor 是如何唤醒后置节点的。
unparkSuccessor 源码:
private void unparkSuccessor(Node node) {
// 获取 节点的 waitStatus
int ws = node.waitStatus;
if (ws < 0)
// 如果 ws <0 ,则cas 操作 ws 更新成 0
compareAndSetWaitStatus(node, ws, 0);
/*
* 获取后置节点
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
// 如果没有后置节点,或者后置节点的 waitStatus >0 (为CANCELLED),则 从队列尾部向前遍历找到最前面的一个 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);
}
unparkSuccessor 代码并不复杂,但是注意,当前节点的后置节点的 waitStatus 为 CANCELLED 时,这时需要找到离当前节点最近的一个非 CANCELLED 状态的节点,这个时候,需要从队尾进行遍历。具体原因可以看下 cancelAcquire 这个方法,这个方法,最后有一行 node.next = node; 相当于将被取消的节点的next 指针指向自己,这个时候如果从 head 遍历,则会出现死循环,而从 tail 开始遍历,则可以正常遍历。
2.3、共享场景的实现
2.3.1、AQS 共享场景下的相关方法
- acquireShared(int arg) 申请获取状态(无法中断)
- acquireSharedInterruptibly(int arg) 申请获取状态(支持中断)
- releaseShared(int arg) 释放状态
我们主要关注 acquireShared(int arg) 、 releaseShared(int arg) 两个方法
2.3.2、acquireShared(int arg)
先附一张流程图(来源:https://ddnd.cn/2019/03/15/java-abstractqueuedsynchronizer/index.html)
acquireShared 源码
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
acquireShared 调用了 tryAcquireShared 和 doAcquireShared 两个方法,同样的,tryAcquireShared 需要由子类实现,在这个方法中改变state ,下面来看下 doAcquireShared 这个方法做了什么。
doAcquireShared 源码
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) {
// 如果前置节点是 head,则尝试获取状态
int r = tryAcquireShared(arg);
if (r >= 0) {
// 这个方法会将node设置为head。
// 如果当前结点acquire到了之后发现还有许可可以被获取,则继续释放自己的后继, 后继会将这个操作传递下去。这就是PROPAGATE状态的含义。
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);
}
}
与互斥场景下的状态申请相比,acquireShared(int arg) 将 acquireQueued(addWaiter(Node.EXCLUSIVE),arg)、selfInterrupt() 统一写在了doAcquireShared 方法中。但是,不同之处是,setHeadAndPropagate 中,会根据传入的 propagate 进行等待节点的唤醒。下面看下 setHeadAndPropagate 的源码。
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
* or after setHead) by a previous operation
* (note: this uses sign-check of waitStatus because
* 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.
*/
// 这里应该是判断是否需要唤醒后置节点,但这里的判断我不是很理解,所以保留了原生的doc,下面是我理解的
//1.propagate > 0 表示调用方指明了后继节点需要被唤醒
//2.头节点后面的节点需要被唤醒(waitStatus<0),不论是老的头结点还是新的头结点
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
// 唤醒后置的共享节点
doReleaseShared();
}
}
其中的doReleaseShared(); 会唤醒线程,这个方法在下面的 releaseShared(int arg) 一起看。
2.3.3、 releaseShared(int arg)
releaseShared(int arg) 源码
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
releaseShared(int arg) 中调用了 tryReleaseShared 、 doReleaseShared,其中tryReleaseShared 需要子类重写,所以我们只关注 doReleaseShared ,看到底是怎么进行线程唤醒的。
doReleaseShared 源码.
private void doReleaseShared() {
for (;;) {
// 获取当前头结点
Node h = head;
if (h != null && h != tail) {
// 如果头结点不为null + 不是尾结点 + 头结点的waitStatus是SIGNAL+CAS 设置waitStatus成功,就唤醒第二个节点
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))
//如果头结点的waitStatus是0,则 CAS 设置 waitStatus 为 PROPAGATE,不成功,则继续循环
continue;
}
if (h == head) // loop if head changed
break;
}
}
注意下,退出循环的条件是 h == head,这样就会出现如下的情况:
当前 :head->a->b->c->d,此时,a 处于阻塞状态
然后,有状态释放,这个时候,a被唤醒,同时,a成功获取了状态,成为了首节点:head(a)->b->c->d 。由于setHeadAndPropagate 时,会唤醒 a 的后置节点 b,b成功获取了状态,随后通过 setHeadAndPropagate 唤醒了 c,但是,c没有获取到状态,重新回到休眠状态。这个时候 a 释放了持有的状态。来个简易图 (为了表示方便,使用 setHAP 代表 setHeadAndPropagate,使用 doRS 代表 doReleaseShared)
a->setHAP->doRS(此时,head 仍为 a) ->releaseShared->doRS(此时,head 为b)
b ->setHAP->doReleaseShared->(此时,head为b)
很明显,a节点所在的线程,在做 releaseShared 时,队列的 head 并不是它,而是 b。a 所在的线程,唤醒的其实是 c。
2.4、对条件变量(Condition)的支持
AQS 提供了Condition 接口的实现类,ConditionObject,每个 ConditionObject 中都维护了条件队列。在分析 ConditionObject 类之前,我们需要理解 AQS 中的同步队列(syn queue)和 ConditionObject 中的条件队列(condition queue)之间的关系。
2.4.1、同步队列 VS 条件队列
同步队列 和 条件队列的锁状态以及联系
同步队列节点:入队(无锁) —> 队列中(获取锁) —>出队(拥有锁)
条件队列节点:入队(拥有锁) —> 队列中 (释放锁) —> 出队(同其他线程争夺锁)
可以明显的看出,同步队列,入队的时候是线程是没有锁的,但是出队的时候,是拥有锁的;相反,条件队列,入队的时候,线程是拥有锁的,在队列中将锁释放,出队的时候,线程已经不再拥有锁了。
一次普通的请求锁+条件等待过程中,节点和队列的变化:
上述的过程中,除了最后的条件达成(signal)时,条件队列中的节点转移到同步队列之外,基本上同步队列 和 条件队列是没有太多交集的。
2.4.2、ConditionObject 的 类结构
// 等待队列的开始节点
private transient Node firstWaiter;
// 等待队列的尾结点
private transient Node lastWaiter;
ConditionObject 只有这两个成员变量,即条件队列的首尾节点,很简单
2.4.3、ConditionObject 的等待(await())
看下,await() 方法的实现,await() 支持中断的处理。
public final void await() throws InterruptedException {
if (Thread.interrupted())
// 检测到中断,直接抛出异常
throw new InterruptedException();
// 将节点加入到条件队列中(源码放在这个方法下面了)
Node node = addConditionWaiter();
// 释放当前的状态
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
// 如果这个节点不在当前在同步队列中,则挂起线程
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 节点加入到 同步队列 中,需要调用 acquireQueued 方法,尝试获取锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
// 这里已经获取到了锁,说明,节点已经从 同步队列中移除,现在需要把通过 unlinkCancelledWaiters 把节点从 条件队列中移除
unlinkCancelledWaiters();
if (interruptMode != 0)
// 根据interruptMode 处理中断
reportInterruptAfterWait(interruptMode);
}
private Node addConditionWaiter() {
// 获取尾结点
Node t = lastWaiter;
// 如果最后一个节点被取消了,则清除取消节点
if (t != null && t.waitStatus != Node.CONDITION) {
// 清除取消节点(源码放下面了)
unlinkCancelledWaiters();
t = lastWaiter;
}
// 创建节点
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
// 初始化头结点
firstWaiter = node;
else
// 将节点放在尾结点后面
t.nextWaiter = node;
// 更新尾结点指针
lastWaiter = node;
return node;
}
// 取消取消状态的节点,这个方法需要在持有锁的情况下,才调用,整个过程并不会出现并发情况,也就没有了线程安全的问题
private void unlinkCancelledWaiters() {
// 获取首节点
Node t = firstWaiter;
// 保存离 firstWaiter 最近的一个(包含 firstWaiter ),状态为 CONDITION 的节点
Node trail = null;
while (t != null) {
// 获取下一个节点
Node next = t.nextWaiter;
if (t.waitStatus != Node.CONDITION) {
// 如果节点的waitStatus 不是 CONDITION,就将这个节点清除掉
// 将节点的后置指针情况
t.nextWaiter = null;
if (trail == null)
// 如果当前尚未找到 trail (说明头结点的状态不是 CONDITION )
firstWaiter = next;
else
// 如果已经找到了 trail,则将 trail 的 nextWaiter 指针指向当前节点的下一个节点(跳过了本节点,相当于把本节点踢出了队列)
trail.nextWaiter = next;
if (next == null)
// 如果当前节点已经是最后一个节点了,则更新 lastWaiter 指针
lastWaiter = trail;
}
else
trail = t;
// 继续下个节点
t = next;
}
}
需要注意的地方:
- await 方法在获取到互斥锁之后调用
- 条件成立后,节点直接从条件队列转移到同步队列
2.4.4、ConditionObject 的唤醒(signal())
signal() 源码:
public final void signal() {
// 判断线程是否持有锁
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 获取条件队列的首节点
Node first = firstWaiter;
if (first != null)
// 唤醒首节点(源码在下边)
doSignal(first);
}
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
// 如果需要唤醒的节点没有后置节点,则直接更新 lastWaiter 指针为null
lastWaiter = null;
// 将first 的 nextWaiter 置为null,帮助GC
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
// 将节点从条件队列 转移到 同步队列
final boolean transferForSignal(Node node) {
/*
* cas 操作失败(已经被转移),返回false
*/
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*
* 节点加到同步队列,并返回前驱结点
*/
Node p = enq(node);
int ws = p.waitStatus;
// 如果被取消或者 前驱结点的 CAS 操作失败,则直接唤醒节点
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
2.4.5、图解 await 和 signal 过程中发生的事
三、参考
- https://segmentfault.com/a/1190000016462281#item-6-11
- https://ddnd.cn/2019/03/15/java-abstractqueuedsynchronizer/
- https://blog.csdn.net/yyzzhc999/article/details/96917878