1. 什么是 Condition?
Condition 是 java.util.concurrent.locks 包下的一个接口,它和 Lock 配合使用,用来实现线程间的等待/通知(wait/notify)机制。可以把 Condition 理解为传统 Object 监视器方法(wait、notify、notifyAll)的现代化升级版,它提供了更精细的控制和更丰富的功能。
简单来说,当线程获取了锁之后,如果某个条件不满足(比如队列为空),就可以调用 Condition.await() 让自己进入等待状态,并释放锁;当另一个线程改变了条件(比如往队列里放了数据),就可以调用 Condition.signal() 或 signalAll() 唤醒等待的线程。
2. 为什么要用 Condition?和 wait/notify 比强在哪?
| 特性 | Object.wait/notify |
Condition |
|---|---|---|
| 锁的绑定 | 必须与 synchronized 配合使用,一个对象只有一个隐式的条件队列。 |
可以与 Lock 配合,一个 Lock 上可以创建多个 Condition 实例,实现多路等待/通知。 |
| 多条件支持 | 一个对象只有一个等待队列,无法区分不同的等待原因(比如"队列已满"和"队列为空"混在一起)。 | 可以创建多个 Condition,例如 notFull 和 notEmpty,分别管理不同条件的等待线程,避免不必要的唤醒。 |
| 中断响应 | wait() 会抛出 InterruptedException,但不能在等待期间响应中断?实际上 wait 是可以响应中断的。但 Condition 提供了更灵活的中断策略:awaitUninterruptibly() 可以在等待时不响应中断。 |
await() 默认响应中断,同时还有 awaitUninterruptibly() 方法。 |
| 超时等待 | 支持 wait(long timeout)。 |
支持更丰富的超时:await(long time, TimeUnit unit)、awaitNanos(long nanosTimeout)、awaitUntil(Date deadline),可以精确控制超时行为。 |
| 公平性 | 等待线程的唤醒顺序不可控(依赖于JVM实现)。 | 如果 Lock 是公平锁,那么 Condition 的等待队列也是公平的,可以按 FIFO 顺序唤醒。 |
| 线程转储 | 通过 synchronized 等待的线程在转储中容易识别。 |
通过 Condition 等待的线程同样会在转储中标记为 WAITING (parking),配合 Lock 信息,也很容易诊断。 |
在复杂的并发组件(比如阻塞队列、线程池)中,Condition 几乎是标配。比如 ArrayBlockingQueue 内部就用了两个 Condition:notEmpty 和 notFull,分别管理"取数据时队列空"和"存数据时队列满"的等待线程。
3. Condition 的核心方法
要使用 Condition,必须先通过 Lock.newCondition() 创建实例。注意:必须在持有对应 Lock 的前提下调用这些方法 ,否则会抛出 IllegalMonitorStateException。
-
void await() throws InterruptedException使当前线程进入等待状态,直到被
signal或中断。调用时会释放锁 ,被唤醒后会重新尝试获取锁 ,获取成功后才会从await返回。 -
void awaitUninterruptibly()等待过程中不响应中断,即使线程被中断也会继续等待,直到被
signal。返回后可以通过Thread.interrupted()检查中断状态。 -
long awaitNanos(long nanosTimeout) throws InterruptedException等待指定的纳秒数,返回值是剩余时间(如果超时则返回 0 或负数)。可以用这个实现超时控制。
-
boolean await(long time, TimeUnit unit) throws InterruptedException等待指定时间,超时返回
false,被唤醒返回true。 -
boolean awaitUntil(Date deadline) throws InterruptedException等待直到某个绝对时间点,超时返回
false,被唤醒返回true。 -
void signal()唤醒一个在此
Condition上等待的线程(如果有多个,选择策略取决于实现,通常是队列头部)。 -
void signalAll()唤醒所有在此
Condition上等待的线程。
4. 经典使用范式:生产者-消费者(多条件)
java
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class BoundedBuffer<T> {
private final Queue<T> queue = new LinkedList<>();
private final int capacity;
private final Lock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition(); // 队列非空条件
private final Condition notFull = lock.newCondition(); // 队列未满条件
public BoundedBuffer(int capacity) {
this.capacity = capacity;
}
public void put(T item) throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity) {
notFull.await(); // 队列已满,等待"非满"条件
}
queue.add(item);
notEmpty.signal(); // 唤醒等待"非空"的消费者
} finally {
lock.unlock();
}
}
public T take() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await(); // 队列为空,等待"非空"条件
}
T item = queue.poll();
notFull.signal(); // 唤醒等待"非满"的生产者
return item;
} finally {
lock.unlock();
}
}
}
关键点:
- 使用
while循环检查条件(而不是if),防止虚假唤醒(spurious wakeup)。这是 Java 并发编程的铁律。 put和take分别操作不同的Condition,避免了像notifyAll那样唤醒所有线程造成的竞争。- 务必在
finally中释放锁。
5. 在 Android 开发中的实践与注意事项
5.1 适用场景
在 Android 中,主线程(UI线程)绝对不能阻塞,所以 Condition 主要用于后台线程之间的协调。常见的场景有:
- 自定义阻塞队列:用于生产者消费者模式,比如处理下载任务的队列、日志写入队列等。
- 线程池的管理:比如自定义线程池,需要根据任务数量控制线程的挂起与唤醒。
- 异步任务的分批处理:比如等待某个条件满足后,再批量执行任务。
5.2 注意事项
- 务必在持有锁时调用
await/signal,否则抛出异常。 await后一定要用while循环检查条件,这是避免虚假唤醒和复杂竞争的必要手段。signal和signalAll的选择 :signal可能更高效,但如果你不确定唤醒哪个线程,或者等待线程可能因为不同原因等待,使用signalAll更安全(但可能会造成"惊群效应")。在多数业务场景中,用signal配合多条件已经足够。- 避免在
await时持有其他锁,否则可能导致死锁。 - 性能考虑 :
Condition基于LockSupport的park实现,比wait/notify更轻量?实际上二者底层实现不同,但性能差异不大。不过Condition的灵活性和可控性带来的收益远大于微小的性能差异。
5.3 与 Kotlin 协程的关系
如果你现在用 Kotlin 开发新项目,很可能不再直接操作线程和 Condition,而是使用协程。Kotlin 协程提供了 Mutex 配合 withLock,以及 Channel、Flow 等高级原语。例如,生产者-消费者可以用 Channel 轻松实现:
kotlin
val channel = Channel<Int>(capacity = 10)
// 生产者
launch { channel.send(42) }
// 消费者
launch { println(channel.receive()) }
Channel 内部也使用了类似 Condition 的机制(在 JVM 上基于 LockSupport 实现),但对开发者完全透明。所以,如果是纯 Kotlin 项目,建议优先使用协程。
但是,如果你在维护老项目,或者需要与 Java 线程池交互,或者编写底层库,Condition 依然是不可或缺的工具。
6. 常见陷阱与调试
- 信号丢失 :如果在调用
signal时没有线程在等待,信号就会丢失。这通常是正确的行为,但有时会被误解。比如,在put时先signal后释放锁,但如果消费者还没开始等待,那么这次signal就浪费了,但没关系,因为数据已经存在,消费者下次take时不会等待。所以,信号是否丢失取决于业务逻辑的设计,需要保证在可能等待之前先持有锁并检查条件。 - 死锁 :如果
await后忘记在另一条路径上signal,就会导致线程永久等待。务必检查所有可能改变条件的地方是否都调用了对应的signal。 - 线程转储分析 :当线程卡住时,通过
jstack可以看到线程状态为WAITING (parking),并能看到它在等待哪个Condition(例如java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)。结合代码,可以快速定位问题。
总结
Condition 是 Java 并发包中为 Lock 量身定做的线程协调利器。相比传统的 wait/notify,它提供了多条件分离、更灵活的等待/唤醒控制、超时机制以及公平性保证。在 Android 后台任务协调、自定义同步组件开发中非常实用。
不过,随着 Kotlin 协程的普及,很多场景已经被更高级的抽象取代。但是,理解 Condition 依然能帮助你深入理解并发编程的本质,也让你在维护底层代码或分析开源框架时游刃有余。