signal vs signalAll 的调度策略和适用场景?
sinal() vs signalAll() 是 Java 并发控制中 Condition 接口的两个核心方法,它们控制的是等待条件队列中线程的唤醒策略。两者在功能、效率与适用场景上存在显著区别。
一、基本概念概览
| 方法名 | 唤醒线程数量 | 调度机制 | 是否受调度顺序影响 |
|---|---|---|---|
signal() | 唤醒一个线程 | FIFO or JVM调度优化 | 有时不可控 |
signalAll() | 唤醒所有线程 | 一次性全部唤醒 | 不依赖顺序 |
二、Condition 的等待队列简要原理
Condition.await() 会让线程进入一个条件队列(ConditionQueue):
- 这些线程会在条件成立之前挂起等待。
- 唤醒这些线程的方式就是通过
signal()或signalAll()。
内部依赖 AQS(AbstractQueuedSynchronizer)实现线程阻塞与唤醒。
三、两者核心差异详解
1. signal():只唤醒队首第一个等待线程
- 通常用于有唯一资源竞争时。
- 如果条件不满足,其它线程仍会保持等待。
示例场景:
lock.lock();
try {
while (!queue.isEmpty()) {
condition.await();
}
// 处理完任务
condition.signal(); // 唤醒下一个
} finally {
lock.unlock();
}
风险:若被唤醒的线程不满足条件,会重新等待,造成伪唤醒浪费。
2. signalAll():唤醒所有等待线程
- 所有在 Condition 上等待的线程都会被唤醒。
- 每个线程醒来后需要重新检查条件(典型的“
while-check”模式)
示例场景:
lock.lock();
try {
// 状态变化所有人都要知道
sharedStateChanged = true;
condition.signalAll(); // 所有人都检查
} finally {
lock.unlock();
}
比如:配置更新、缓存重建、所有消费者都可能受影响。
四、为什么推荐 signalAll() 搭配 while?
因为:
- 虚假唤醒(Spurious Wakeup):JVM 可能随时唤醒线程;
- 条件竞争:唤醒后发现条件仍不满足的可能性极高。
while (!条件满足) {
condition.await();
}
五、性能对比和调度策略
| 项目 | signal() | signalAll() |
|---|---|---|
| 唤醒线程数 | 最多一个 | 所有等待线程 |
| 开销(线程调度) | 较低 | 较高,可能引发“惊群效应” |
| 是否适合高频调度场景 | 是 | 否 |
| 使用前提 | 队列中线程可独立条件检查 | 唤醒后必须重新验证共享状态 |
| 推荐使用的典型场景 | 队列模型 / 单个资源竞争 | 多线程依赖同一共享条件的变化 |
六、实际应用场景分析
| 场景 | 推荐方法 | 原因说明 |
|---|---|---|
| 生产者-消费者模型 | signal() | 只需要唤醒一个消费者或生产者,避免惊群 |
| 资源池管理(连接池) | signal() | 获取连接的线程一个一个唤醒即可 |
| 配置中心更新通知 | signalAll() | 配置变更所有依赖线程都需要知道 |
| 线程屏障(如 CountDownLatch) | signalAll() | 所有等待线程同时继续执行 |
| 全局错误状态切换 | signalAll() | 所有等待线程都应立即被唤醒响应 |
七、源码解析(简要)
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
public final void signalAll() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignalAll(first);
}
内部会将条件队列中的线程节点转移到 AQS 的同步队列中,唤醒顺序视 JVM 线程调度而定。
八、实战对比案例(简化)
示例:线程 A/B/C 在等待共享条件
// 线程 A/B/C
lock.lock();
try {
while (!dataReady) {
condition.await(); // 全部进入 Condition 队列
}
// 执行读取
} finally {
lock.unlock();
}
使用 signal():
// 通知线程
lock.lock();
dataReady = true;
condition.signal(); // 只唤醒一个线程
lock.unlock();
- ➕ 轻量、节省 CPU
- ➖ 剩下的线程继续阻塞,若业务逻辑需多线程反应,就不足
使用 signalAll():
lock.lock();
dataReady = true;
condition.signalAll(); // 唤醒所有线程
lock.unlock();
- ➕ 所有线程检查条件,适合同步更新
- ➖ 所有线程被唤醒但可能又马上 await,浪费 CPU(惊群)
九、权威资料参考
- Java 官方文档 – Condition 接口
- 《Java Concurrency in Practice》 — 第 14 章
- OpenJDK AbstractQueuedSynchronizer 源码
总结口诀
signal 一人起,轻量少冲击;
signalAll 皆唤醒,状态统一齐。
条件判断用 while,虚假唤醒防冲击。
更多详细内容请关注其他相关文章!