ReentrantLock条件队列源码精讲:精准等待唤醒机制全解析
摘要
Condition接口解决了原生等待唤醒机制的缺陷,基于AQS实现了独立的条件等待队列。它允许线
在并发编程中,掌握互斥锁和共享限流后,线程间的等待与唤醒机制是实现复杂协作逻辑的关键。无论是生产者消费者模型还是顺序执行控制,都需要一套可靠高效的等待唤醒工具。Java原生的Object.wait()和notify()虽然基础,但其单条件队列、无法精准唤醒、易发生虚假唤醒等设计局限,在高并发场景下难以满足性能与可靠性要求。
JUC包中的Condition接口,正是为克服这些缺陷而设计。作为ReentrantLock的黄金搭档,它基于AQS(AbstractQueuedSynchronizer)框架,提供了多条件、精准化的等待唤醒能力,是AQS四大核心组件(同步队列、独占模式、共享模式、条件队列)中最后一块,也是最精妙的一块拼图。

一、开篇先纠偏:Condition 解决了什么核心问题?
理解Condition的价值,需要从原生机制的痛点出发。想象所有等待线程都挤在同一个监视器队列中,唤醒时只能“一锅端”,无法区分需要唤醒的是生产者还是消费者,导致效率低下且易出错。
Condition的核心贡献在于,它为AQS引入了独立于主同步队列的“条件等待队列”。当线程因业务条件不满足时,可以释放锁并进入指定的条件队列等待;当条件满足时,又能被精准唤醒,重新加入同步队列竞争锁。这种设计将等待的粒度从“锁”级别细化到“条件”级别,是并发控制模型的一次重要升级。
二、核心铺垫:AQS 条件队列基础结构
Condition的核心实现是AQS的内部类ConditionObject。其设计巧妙,完全复用了AQS的节点(Node)结构和LockSupport的阻塞唤醒机制,没有引入冗余设计。
public abstract class AbstractQueuedSynchronizer {
// Condition 核心实现类
public class ConditionObject implements Condition, ja va.io.Serializable {
// 条件队列:单向链表(区别于同步队列的双向链表)
private transient Node firstWaiter;
private transient Node lastWaiter;
public ConditionObject() {}
}
// AQS 同步队列节点(复用,条件队列共用 Node 结构)
static final class Node {
// 节点状态:CONDITION 表示节点在条件队列中
static final int CONDITION = -2;
// 条件队列下一个等待节点
Node nextWaiter;
}
}
这里有三个关键设计点值得深入理解:
- 一锁多条件:一个
Lock可以创建多个Condition对象,每个Condition都拥有自己独立的单向链表作为条件队列。 - 状态区分:通过
Node.waitStatus = CONDITION标记节点正处于条件队列中,与同步队列节点状态明确区分。 - 流程闭环:线程抢锁成功 → 发现条件不满足 → 释放锁 → 进入条件队列等待 → 被
signal唤醒 → 节点转移回同步队列 → 重新抢锁 → 继续执行业务。整个过程形成一个安全、高效的闭环。
三、Condition 核心源码解析
理解了骨架,我们深入其血肉,聚焦三个核心方法:await()(等待)、signal()(唤醒单个)和signalAll()(唤醒全部)。
1. 核心等待:await() 源码
await()是线程进入等待状态的核心入口,其逻辑环环相扣:
public final void await() throws InterruptedException {
// 1. 响应中断,线程中断直接抛异常
if (Thread.interrupted())
throw new InterruptedException();
// 2. 将当前线程封装为 CONDITION 状态节点,加入条件队列尾部
Node node = addConditionWaiter();
// 3. 释放锁(必须释放,否则死锁),返回释放前的 state
long sa vedState = fullyRelease(node);
boolean interrupted = false;
// 4. 循环判断:节点是否回到同步队列,未回到则持续阻塞
while (!isOnSyncQueue(node)) {
// 4.1 阻塞线程(LockSupport.park())
LockSupport.park(this);
// 4.2 被唤醒后检查中断
if ((interrupted = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 5. 被唤醒后,重新进入 AQS 同步队列抢锁(独占模式)
if (acquireQueued(node, sa vedState) && interrupted != THROW_IE)
interrupted = REINTERRUPT;
// 6. 清理取消的节点
if (node.nextWaiter != null)
unlinkCancelledWaiters();
// 7. 处理中断状态
if (interrupted != 0)
reportInterruptAfterWait(interrupted);
}
其中,addConditionWaiter()负责创建并加入条件队列节点。一个关键细节是:await()方法必须在持有锁的情况下调用,否则后续的fullyRelease()会抛出IllegalMonitorStateException。调用await()会完全释放锁,这确保了其他线程能够获取锁来修改条件变量。线程在阻塞期间只存在于条件队列,不占用任何锁资源。
2. 核心唤醒:signal() 源码
唤醒操作的核心是将节点从条件队列“搬运”到同步队列。
public final void signal() {
// 1. 校验:必须持有锁才能调用 signal()
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
// 2. 唤醒条件队列第一个节点
if (first != null)
doSignal(first);
}
// 辅助方法:执行唤醒(转移节点到同步队列)
private void doSignal(Node first) {
do {
// 移除条件队列头节点
if ((firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
// 3. 将节点从条件队列转移到同步队列,失败则继续唤醒下一个
} while (!transferForSignal(first) && (first = firstWaiter) != null);
}
// 核心:转移节点到同步队列
final boolean transferForSignal(Node node) {
// 1. CAS 将节点状态从 CONDITION 改为 0
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 2. 将节点加入 AQS 同步队列尾部
Node p = enq(node);
int ws = p.waitStatus;
// 3. 唤醒线程,使其进入同步队列抢锁
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
3. 全量唤醒:signalAll() 源码
signalAll()的逻辑与signal()类似,区别在于遍历整个条件队列,将所有有效节点都转移到同步队列。
public final void signalAll() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
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);
}
必须明确一个关键规则:signal()或signalAll()仅完成节点的转移和线程的唤醒(通过LockSupport.unpark),被唤醒的线程需要重新进入同步队列竞争锁,这保证了线程安全性和操作的原子性。
四、ReentrantLock 中的 Condition 实现
在实际使用中,我们通常通过ReentrantLock获取Condition实例,其实现简洁明了,直接复用了AQS的ConditionObject。
public class ReentrantLock implements Lock {
private final Sync sync;
// 创建 Condition 实例,绑定当前锁
public Condition newCondition() {
return sync.newCondition();
}
abstract static class Sync extends AbstractQueuedSynchronizer {
final ConditionObject newCondition() {
return new ConditionObject();
}
}
}
五、个性化工程感悟
初看Condition,可能觉得它只是wait/notify的“升级版”。但深入其设计,你会发现这背后是AQS对“线程同步粒度”的极致追求。原生wait/notify将所有等待线程塞进一个队列,唤醒时难免“误伤”。而Condition允许我们将等待线程按不同的业务条件(如“队列空”和“队列满”)分组到不同的队列中,实现精准的、按需的唤醒。
这种“分而治之”、“精准调度”的思想,其价值远超线程同步本身。它在高并发任务调度、消息队列路由、事件驱动架构中都有深刻体现,是构建高性能、高可控性系统的重要思维模型。
六、场景化延伸 + 实战指引(落地可执行)
1. 场景化延伸:Condition 核心适用场景
- 生产者消费者模型:创建两个
Condition,分别用于生产者在队列满时等待,消费者在队列空时等待,实现精准通知。 - 多条件顺序执行:控制线程A、B、C必须按顺序执行,每个线程执行完后唤醒下一个,避免无效的全局唤醒。
- 自定义同步器:实现带有复杂业务条件的锁,例如带超时机制、带权限校验的锁。
- 池化资源等待:数据库连接池、线程池在资源耗尽时,让请求线程在特定条件队列中等待,有资源释放时再精准唤醒。
2. 实战避坑技巧(高频必坑)
- 坑点1:未加锁调用 await()/signal():这是最常见的错误,必定抛出
IllegalMonitorStateException。务必在lock()和unlock()之间调用这些方法。 - 坑点2:await() 不处理中断:如果业务不关心中断,建议使用
awaitUninterruptibly()方法,或者妥善捕获InterruptedException,避免线程意外退出导致状态不一致。 - 坑点3:混淆 signal() 和 signalAll():只唤醒一个等待线程时用
signal(),需要唤醒所有等待同一条件的线程时用signalAll()。错误使用可能导致线程永久等待。 - 坑点4:忘记唤醒导致线程阻塞:这是一个逻辑错误。在修改了条件变量后,必须记得调用对应的
signal()或signalAll(),否则条件队列中的线程将永远沉睡。
3. 实战标准模板(生产者消费者,直接复制)
下面是一个经典的生产者消费者模型实现,清晰地展示了双Condition的用法:
ReentrantLock lock = new ReentrantLock();
// 空队列条件:消费者等待
Condition emptyCond = lock.newCondition();
// 满队列条件:生产者等待
Condition fullCond = lock.newCondition();
Queue
七、核心总结
总结来说,Condition是JUC提供的一套工业级、精准化的线程等待唤醒机制。它基于AQS独立的条件队列实现,核心流程清晰严谨。必须与ReentrantLock配合使用,且所有操作都需在持有锁的上下文中进行。相较于原始的wait/notify,它在支持多条件队列、精准唤醒、灵活中断处理等方面具有显著优势,是现代Java高并发编程中处理复杂线程协作的首选工具。掌握其原理和最佳实践,对于构建健壮、高效的多线程应用至关重要。
来源:互联网
本网站新闻资讯均来自公开渠道,力求准确但不保证绝对无误,内容观点仅代表作者本人,与本站无关。若涉及侵权,请联系我们处理。本站保留对声明的修改权,最终解释权归本站所有。