本篇文章最好结合《深入剖析 ReentrantLock》一起看
在使用 synchronized 保证线程安全时,如果想要实现线程的等待/通知机制(手动阻塞和唤醒线程),我们可以使用 Object.wait()/Object.notify() 来操作,一般可以通过这两个方法实现一个自定义的基于 生产者/消费者模型 的阻塞队列。
而在上篇文章《深入剖析 ReentrantLock》我们了解到,Lock是基于 AQS (AbstractQueuedSynchronizer) 来实现 加锁/解锁 以及管理线程的 阻塞/唤醒:
-
加锁/解锁:volatile 修饰 state 来管理锁的状态,加锁解锁的操作是通过 CAS 操作 state 值实现;
-
线程的阻塞/唤醒:维护一个先入先出的双向队列(下文称「同步队列」)来管理未抢到锁的线程,并且有一套机制来进行线程的阻塞和唤醒。
AQS 基于 ConditionObject 实现获取锁后线程的手动 阻塞/唤醒(注意,未获取到锁的线程调用 Condition 的 await() 和 signal() 相关的方法会抛出异常,下文会分析),ConditionObject 在 AQS 中以内部类的形式存在,各个 Lock 工具类都提供新建的入口,比如 ReentrantLock 中提供 newCondition() 方法创建 Condition:
final ConditionObject newCondition() {
return new ConditionObject();
}
ConditionObject 内部维护了一个单向链表来实现线程的 等待/通知机制:
// 条件队列头节点
private transient Node firstWaiter;
// 条件队列尾节点
private transient Node lastWaiter;
Node 结构:
static final class Node {
// 节点状态,加入条件队列节点状态为-2
volatile int waitStatus;
// 「同步队列」节点的前继节点
volatile Node prev;
// 「同步队列」节点的后面继节点
volatile Node next;
// 线程
volatile Thread thread;
// 「条件队列」的后继节点
Node nextWaiter;
}
一个 Condition 就维护着一个「条件队列」,不同 Condition 之间的「条件队列」是互相隔离的。相比于 synchronized,多个「条件队列」会让 唤醒/阻塞 的粒度更小。
Condition 中的阻塞机制
await() 方法是会让当前线程加入到调用者 Condition 的「条件队列」里,最后释放锁并阻塞线程,和 Object.wait() 方法基本一致。
await() 方法源码:
public final void await() throws InterruptedException {
// 判断当前线程是否中断并清空中断标志
if (Thread.interrupted())
throw new InterruptedException();
// 将当前线程加入到「条件队列」
Node node = addConditionWaiter();
// 释放所有锁,将state修改为0
int savedState = fullyRelease(node);
int interruptMode = 0;
// 判断当前node是否在「同步队列」内
while (!isOnSyncQueue(node)) {
// 进行线程阻塞
LockSupport.park(this);
// 线程被唤醒后执行到这里,先检查中断状态
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 尝试获取锁直到获取锁成功
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
await() 方法的逻辑是:先判断线程的中断状态,接着将当前线程以 Codition 状态加入到 CoditionObject「条件队列」的尾部,然后释放锁,最后阻塞当前线程。后续线程被唤醒后会调用 acquireQueued() 争抢锁。
我们来看下内部的方法细节:
addConditionWaiter() 方法,加入「条件队列」尾部:
private Node addConditionWaiter() {
Node t = lastWaiter;
// 如果「条件队列」尾部已取消就清理掉
if (t != null && t.waitStatus != Node.CONDITION) {
// 清理Cancelled状态节点
unlinkCancelledWaiters();
t = lastWaiter;
}
// 初始化当前线程的CONDITION状态节点
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;
Node trail = null;
while (t != null) {
Node next = t.nextWaiter;
// 如果节点状态不为 -2,就断开节点
if (t.waitStatus != Node.CONDITION) {
t.nextWaiter = null;
if (trail == null)
firstWaiter = next;
else
trail.nextWaiter = next;
if (next == null)
lastWaiter = trail;
}
else
trail = t;
t = next;
}
}
fullyRelease() 方法,释放锁,将 state 值修改为 0:
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState();
// 将 state 修改为 0
// 如果当前线程没有持有锁并调用await方法会抛出IllegalMonitorStateException异常
if (release(savedState)) {
failed = false;
return savedState;
} else {
// 如果释放锁失败抛出异常
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
// 走到这里代表非持有锁的线程调用await方法或释放锁失败
// 将节点状态设置为取消,等待后续懒加载的方式清理
node.waitStatus = Node.CANCELLED;
}
}
因为释放锁的过程无竞争,所以无需 CAS 操作 state 保证原子,直接修改即可。释放锁的时候如果发现非占用线程调用会抛出异常。
acquireQueued() 方法,获取锁:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 自旋
for (;;) {
// 获取node节点的前继节点
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);
}
}
你可能会发现 acquireQueued() 方法里争抢锁的前提是 node 节点需要在「同步队列」里,之前分析了调用 await() 方法是会把线程节点放到「条件队列」的尾部,那是何时放到「同步队列」的呢?我们看下 signal() 方法寻找下答案。
Condition 中的唤醒机制
signal() 方法会唤醒「条件队列」的头部节点去争抢锁:
public final void signal() {
// 如果非持有锁的线程调用 signal 方法则会抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
// 唤醒「条件队列」头节点
doSignal(first);
}
doSignal() 方法的作用就是将「条件队列」中第一个非取消状态的节点移动到「同步队列」参与锁的竞争:
private void doSignal(Node first) {
do {
// 断开当前节点并遍历到下个节点
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
// 转移node节点到「同步队列」内,如果不成功执行到do代码块内
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
transferForSignal() 方法将 node 节点转移到「同步队列」内参与锁的争抢:
final boolean transferForSignal(Node node) {
// 通过CAS去修改节点状态为初始化状态0
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
// 修改失败代表节点已经取消,返回false唤醒下一个节点
return false;
// 将当前节点加到「同步队列」尾部,并返回当前节点在「同步队列」中的前继节点
Node p = enq(node);
int ws = p.waitStatus;
// 如果前继节点已经取消或设置唤醒状态失败则唤醒节点
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
transferForSignal() 方法就是 signal() 方法的核心,通过 CAS 的方式去修改节点状态为初始化状态 0(后续会从「条件队列」清理掉),然后转移到到「同步队列」的尾部。
可以看出来 Condition.signal() 方法的唤醒还是比较公平的,而 Object.notify() 是随机选一个线程进行唤醒,这也是两者最大的区别;源码里注释也是The choice is arbitrary:这个选择是随机的:
signalAll() 方法的作用是转移「条件队列」里所有非取消状态的节点到「同步队列」里参与争抢锁:
public final void signalAll() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
// 调用doSignalAll方法对「条件队列」从头开始遍历,转移到「同步队列」尾部
doSignalAll(first);
}
private void doSignalAll(Node first) {
lastWaiter = firstWaiter = null;
do {
// 暂存后继节点
Node next = first.nextWaiter;
first.nextWaiter = null;
// 转移节点
transferForSignal(first);
// 遍历到下个节点
first = next;
} while (first != null); // 直到遍历完成
}
Condition 中的超时阻塞机制
Condition 也支持调用 await 方法超时自动唤醒的 API:await(long, TimeUnit),awaitNanos(long),awaitUntil(Date)。
实现上都差不多,我们选择 await(long, TimeUnit) 来分析下:
public final boolean await(long time, TimeUnit unit)
throws InterruptedException {
// 计算超时纳秒的时间戳
long nanosTimeout = unit.toNanos(time);
// 如果线程中断抛出中断异常
if (Thread.interrupted())
throw new InterruptedException();
// 将当前线程加入到「条件队列」
Node node = addConditionWaiter();
// 释放所有锁,将 state 修改为0,如果当前线程非锁的持有者就抛出异常
int savedState = fullyRelease(node);
// 计算超时时间戳
final long deadline = System.nanoTime() + nanosTimeout;
boolean timedout = false;
int interruptMode = 0;
// 如果节点不在「同步队列」里进入循环
while (!isOnSyncQueue(node)) {
// 判断是否达到超时时间
if (nanosTimeout <= 0L) {
// 如果是,将当前节点转移到「同步队列」尾部,参与锁的争抢
timedout = transferAfterCancelledWait(node);
break; // 跳出循环
}
// 如果超时时间大于等于 1000 纳秒就直接调用 LockSupport.parkNanos 方法超时阻塞
if (nanosTimeout >= spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
// 如果超时时间小于 1000 纳秒就自旋直到加入到「同步队列」尾部
nanosTimeout = deadline - System.nanoTime();
}
// 参与争抢锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null)
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
return !timedout;
}
await(long, TimeUnit) 方法的逻辑和之前《深入剖析 ReentrantLock》中讲的 tryLock(long, TimeUnit) 方法逻辑类似:
1. 计算出超时纳秒的时间戳;
2. 判断线程中断,如果中断抛出异常;
3. 将当前线程加入到「条件队列」;
4. 释放锁并校验调用线程的合法性(是否持有锁);
5. 再次计算超时时间戳并判断超时时间是否已到达,如果是就将线程节点转移到「同步队列」尾部;
6. 如果超时时间没到,就判断是否小于spinForTimeoutThreshold(1000纳秒),如果是进行自旋,否则阻塞(这里也同样是个优化,针对超时时间短的线程进行自旋来尝试加到「同步队列」尾部来减少阻塞带来的开销)。
Lock 的整体流程总结如下:
如果觉得文章不错可以点个赞和关注!
公众号:阿东编程之路
你好,我是阿东,目前从事后端研发工作。技术更新太快需要一直增加自己的储备,索性就将学到的东西记录下来同时分享给朋友们。未来我会在此公众号分享一些技术以及学习笔记之类的。妥妥的都是干货,跟大家一起成长。