Java Condition 协同机制实战破局(附完整案例)
目录导读
- Condition 是什么?为什么需要它?
- 核心 API 解析:await / signal / signalAll 的默契配合
- 经典案例一:多生产者-多消费者模型的精准唤醒
- 经典案例二:有界队列的“非满”与“非空”双条件等待
- Condition 与 synchronized + wait/notify 的本质区别
- 高频面试问答:Condition 的坑与最佳实践
- 性能与替代方案:何时用 Lock 而非内置锁
Condition 是什么?为什么需要它?
在 Java 并发编程中,synchronized 配合 wait() / notify() 一直是线程协作的基石,但它的局限在于:只有一个隐式条件队列,这意味着,当多个线程因不同原因等待时,notifyAll() 会唤醒所有线程,导致大量“虚假唤醒”和上下文切换开销。

java.util.concurrent.locks.Condition 接口正是为解决此痛点而生,它允许一个 Lock 创建多个条件队列(即多个 Condition 实例),每个队列独立管理一组等待线程,你可以精确地唤醒某一类线程,而不会打扰其他无关线程,这在阻塞队列、连接池、限流器等场景中价值巨大。
核心 API 解析:await / signal / signalAll 的默契配合
在使用 Condition 前,需要先获取一个 Lock 实例:
Lock lock = new ReentrantLock(); Condition notFull = lock.newCondition(); // 队列未满的条件 Condition notEmpty = lock.newCondition(); // 队列非空的条件
关键方法:
| 方法 | 作用 | 注意事项 |
|---|---|---|
await() |
当前线程释放锁并等待,直到被 signal。 | 必须持有锁;支持中断响应。 |
await(long time, TimeUnit unit) |
限时等待,超时自动返回。 | 返回 false 表示超时。 |
signal() |
唤醒一个在该 Condition 上等待的线程。 | 需要先持有对应的 Lock。 |
signalAll() |
唤醒所有在该 Condition 上等待的线程。 | 比 signal 更安全但开销更大。 |
❗ 核心铁律:
await()和signal()必须在lock.lock()和lock.unlock()之间调用,否则抛出IllegalMonitorStateException。
经典案例一:多生产者-多消费者模型的精准唤醒
想象一个仓库,容量为 10,生产者满时等待,消费者空时等待,传统 synchronized 需要 notifyAll() 唤醒所有线程,而生产者被唤醒后可能发现仓库仍满,再次等待,造成无效竞争。
使用 Condition 的优雅解法:
public class Warehouse {
private final int capacity;
private int count = 0;
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
public Warehouse(int capacity) { this.capacity = capacity; }
public void put() throws InterruptedException {
lock.lock();
try {
while (count == capacity) {
notFull.await(); // 仓库满,生产者等待
}
count++;
System.out.println(Thread.currentThread().getName() + " 生产,库存:" + count);
notEmpty.signal(); // 精准唤醒一个消费者
} finally {
lock.unlock();
}
}
public void take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await(); // 仓库空,消费者等待
}
count--;
System.out.println(Thread.currentThread().getName() + " 消费,库存:" + count);
notFull.signal(); // 精准唤醒一个生产者
} finally {
lock.unlock();
}
}
}
运行逻辑优势:
- 生产者满时只进
notFull队列等待,消费者只进notEmpty队列等待。 signal()只唤醒对应队列中的一个线程,无多余竞争。
经典案例二:有界队列的“非满”与“非空”双条件等待
这是 JDK 中 ArrayBlockingQueue 的内部实现思路,值得手写一遍加深理解:
public class BoundedBuffer<T> {
private final Object[] items;
private int putIndex, takeIndex, count;
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
public BoundedBuffer(int capacity) {
items = new Object[capacity];
}
public void put(T item) throws InterruptedException {
lock.lock();
try {
while (count == items.length) notFull.await();
items[putIndex] = item;
if (++putIndex == items.length) putIndex = 0;
count++;
notEmpty.signal(); // 唤醒一个阻塞的 take 线程
} finally {
lock.unlock();
}
}
@SuppressWarnings("unchecked")
public T take() throws InterruptedException {
lock.lock();
try {
while (count == 0) notEmpty.await();
T item = (T) items[takeIndex];
if (++takeIndex == items.length) takeIndex = 0;
count--;
notFull.signal(); // 唤醒一个阻塞的 put 线程
} finally {
lock.unlock();
}
return item;
}
}
注意细节:
- 使用
while而非if检查条件,防止虚假唤醒(spurious wakeup)。 - 通过环形数组避免数据搬移,提升性能。
- 每个 Condition 只负责一个方向的阻塞,逻辑清晰。
Condition 与 synchronized + wait/notify 的本质区别
| 维度 | synchronized | Lock + Condition |
|---|---|---|
| 条件队列数量 | 只有一个 | 可创建多个 |
| 可中断等待 | 不支持 | 支持 awaitInterruptibly() |
| 超时等待 | 需手动循环 | 原生支持 await(long, TimeUnit) |
| 公平性 | 不保证 | 可由 Lock 构造函数控制 |
| 精准唤醒 | 只能 notifyAll() 或随机一个 notify() | 按条件精准 signal() |
何时用 Condition?
当“等待原因不止一种”时(如队列满和队列空),或者需要超时/中断控制,Condition 是不二之选,若简单场景,synchronized 足够。
高频面试问答:Condition 的坑与最佳实践
Q1:为什么 await() 必须放在 while 循环里而不是 if?
A:因为线程可能被虚假唤醒(操作系统层面允许),且即使被 signal() 唤醒,也不能保证条件一定满足(例如另一个线程抢先消费了)。while 循环会重新检查条件,保证安全。
Q2:signal() 和 signalAll() 选哪个?
A:如果多个线程等待条件相同且唤醒一个就能满足需求,用 signal() 效率更高,但若不确定,或同一条件有多个线程在等待且都需继续,则用 signalAll(),错误使用 signal() 可能导致死锁——如果唤醒的线程发现条件仍不满足,可能永远无法被再次唤醒。
Q3:Condition 能否脱离 Lock 单独使用?
A:不能,Condition 实例必须通过 lock.newCondition() 创建,且其 await/signal 必须基于持有该 Lock 的线程,条件队列与 Lock 绑定。
Q4:await() 被中断会怎样?
A:会抛出 InterruptedException,线程恢复到就绪状态,且重新获取锁后才会抛出,可使用 awaitUninterruptibly() 忽略中断。
Q5:与 Object.wait() 相比,Condition 是否一定更快?
A:不一定,取决于场景。synchronized 在无竞争时可能更轻量,但在高竞争、多条件复杂场景下,Condition 减少无效唤醒,系统吞吐量更高。
性能与替代方案:何时用 Lock 而非内置锁
- 性能:JDK 6 之后
synchronized已经经过锁升级优化(偏向锁、轻量级锁),与 ReentrantLock 性能差别不大,但 Condition 在多条件场景下优势明显,因为它减少了上下文切换和无效线程调度。 - 可操作性:需要超时控制、中断响应、公平锁、多条件队列时,用
Lock + Condition。 - 替代方案:在 Java 8 之后的
Stream或CompletableFuture中,很多并发协作可直接使用Semaphore、CountDownLatch、CyclicBarrier等工具,它们内部也基于 AQS(AbstractQueuedSynchronizer),但不需要手动管理 Condition。