1. 为什么需要Condition机制
在Java多线程编程中,我们最常用的同步工具是synchronized关键字和Object类的wait()/notify()机制。但这种方式存在几个明显的局限性:
- 无法区分等待条件:一个锁对象只能有一个等待队列,当不同线程等待不同条件时,无法精确唤醒特定线程。
- 过早唤醒问题:notifyAll()会唤醒所有等待线程,即使它们等待的条件尚未满足。
- 锁机制不灵活:synchronized是独占锁,无法实现更复杂的锁策略。
Condition接口正是为了解决这些问题而设计的。它提供了更精细的线程等待/通知机制,允许一个锁关联多个等待条件队列。这就像医院的分诊系统——普通病人和急诊病人会被分到不同的等待区,医生可以根据病情严重程度有针对性地叫号,而不是像传统方式那样对所有病人一视同仁。
实际开发中,当你的业务逻辑需要基于多个不同条件进行线程等待时,Condition机制能大幅提升程序的效率和可维护性。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Condition的核心原理剖析
2.1 AQS与Condition的关系
Condition的实现依赖于AbstractQueuedSynchronizer(AQS),这是Java并发包的核心基础组件。每个Condition对象都维护了一个独立的等待队列(不同于锁的同步队列),这个设计使得:
- 一个Lock可以创建多个Condition
- 每个Condition管理自己独立的等待线程队列
- 线程可以在不同的条件队列间转移
java复制// 典型使用示例
Lock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition();
Condition notFull = lock.newCondition();
2.2 await()的内部工作流程
当线程调用await()时,会发生以下原子性操作:
- 线程释放持有的锁(这是await()能正常工作的前提)
- 创建节点加入Condition的等待队列
- 将线程置为WAITING状态
- 等待被signal()唤醒或中断
这个过程中最容易被忽视的是第1步——如果线程不持有锁就调用await(),会直接抛出IllegalMonitorStateException。这与Object.wait()的行为一致,但新手常常在此处犯错。
2.3 signal()的唤醒机制
signal()的工作流程同样精妙:
- 将等待时间最长的节点(FIFO队列的首节点)从Condition队列移到锁的同步队列
- 被转移的线程不会立即执行,而是需要重新竞争锁
- 只有成功获取锁后,该线程才会从await()调用处恢复执行
这里有个关键点:signal()只是将线程从等待队列移到同步队列,并不保证该线程能立即获取锁。这意味着被唤醒的线程可能需要与其他线程竞争锁资源,这也是为什么Condition的使用通常需要放在循环检查中:
java复制while (!conditionMet) {
cond.await();
}
3. 实战中的Condition使用模式
3.1 经典的生产者-消费者实现
下面是一个使用Condition实现的有界队列,它比使用wait()/notify()的版本更清晰:
java复制public class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await();
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
这个实现展示了Condition的两个关键优势:
- 可以明确区分"队列非空"和"队列非满"两种等待条件
- 唤醒操作更有针对性(signal()而非signalAll())
3.2 性能优化技巧
-
选择合适的唤醒方式:
- signal():当确定只有一个等待线程需要被唤醒时使用
- signalAll():当条件变化可能满足多个等待线程时使用
-
避免嵌套Condition:
虽然技术上可行,但嵌套使用Condition(在一个Condition的await()回调中调用另一个Condition的方法)极易导致死锁,应尽量避免。 -
超时控制:
总是优先使用await(long time, TimeUnit unit)而非无限等待,这能提高系统健壮性:
java复制if (!conditionMet) {
if (!cond.await(500, TimeUnit.MILLISECONDS)) {
// 超时处理逻辑
}
}
4. 常见问题与深度排查
4.1 为什么await()必须在循环中调用?
这是Condition使用中最常见的误区。很多开发者会写出这样的代码:
java复制if (!conditionMet) { // 错误用法!
cond.await();
}
这种写法存在"虚假唤醒"风险——即使没有调用signal(),线程也可能从await()返回。正确的做法是:
java复制while (!conditionMet) { // 正确用法
cond.await();
}
这种模式被称为"Guarded Suspension"模式,是并发编程中的基础范式之一。
4.2 await()与sleep()的本质区别
虽然都能让线程暂停,但两者有根本区别:
| 特性 | await() | sleep() |
|---|---|---|
| 锁状态 | 释放锁 | 不释放锁 |
| 唤醒条件 | signal()/signalAll()或中断 | 时间到期或中断 |
| 使用场景 | 线程间协作 | 单纯的时间等待 |
| 异常处理 | 需要处理InterruptedException | 需要处理InterruptedException |
4.3 死锁排查案例
考虑下面这个错误用法:
java复制// 线程A
lock.lock();
try {
cond.await(); // 释放锁并等待
// 执行某些操作
} finally {
lock.unlock();
}
// 线程B
cond.signal(); // 错误!没有持有锁
这个代码会导致:
- 线程B调用signal()时未持有锁,抛出IllegalMonitorStateException
- 即使异常被捕获,线程A也永远无法被唤醒,形成死锁
正确的做法是:
java复制// 线程B
lock.lock();
try {
cond.signal();
} finally {
lock.unlock();
}
这个案例告诉我们:signal()/signalAll()和await()一样,必须在持有锁的情况下调用。
5. 高级应用与性能考量
5.1 Condition与性能优化
在高度竞争的环境中,Condition的性能优化尤为重要:
-
减少signalAll()的使用:
不必要的signalAll()会导致"惊群效应",大量线程被唤醒但只有一个能获取锁,造成资源浪费。 -
条件检查优化:
将最可能失败的条件检查放在最前面:
java复制while (!condition1 || !condition2) { // condition1更可能为false
cond.await();
}
- 考虑使用Condition还是LockSupport:
对于更底层的控制,LockSupport.park()/unpark()可能更高效,但失去了Condition的条件队列管理能力。
5.2 与Java内存模型的关系
Condition的await()/signal()机制建立了happens-before关系:
- 线程A调用signal()前的所有操作对被唤醒的线程B可见
- 这类似于volatile变量的内存语义,但粒度更细
理解这一点对编写正确的并发程序至关重要。例如:
java复制// 线程A
lock.lock();
try {
sharedVar = 42;
cond.signal();
} finally {
lock.unlock();
}
// 线程B
lock.lock();
try {
while (sharedVar != 42) {
cond.await();
}
// 这里可以安全地使用sharedVar
} finally {
lock.unlock();
}
5.3 与其他并发组件的配合
Condition常与其他并发工具配合使用:
-
与Future结合:
可以实现更灵活的异步结果等待机制。 -
与CountDownLatch结合:
可以构建复杂的阶段同步点。 -
在ThreadPoolExecutor中的应用:
标准线程池使用Condition实现任务队列的等待/通知机制。
我在实际项目中曾遇到过这样一个案例:需要实现一个可暂停的任务队列。通过Condition与ReentrantLock的组合,我们实现了优雅的暂停/恢复机制:
java复制public class PausableExecutor extends ThreadPoolExecutor {
private final Lock pauseLock = new ReentrantLock();
private final Condition unpaused = pauseLock.newCondition();
private boolean isPaused;
// 构造函数省略...
protected void beforeExecute(Thread t, Runnable r) {
pauseLock.lock();
try {
while (isPaused) {
unpaused.await();
}
} catch (InterruptedException ie) {
t.interrupt();
} finally {
pauseLock.unlock();
}
}
public void pause() {
pauseLock.lock();
try {
isPaused = true;
} finally {
pauseLock.unlock();
}
}
public void resume() {
pauseLock.lock();
try {
isPaused = false;
unpaused.signalAll();
} finally {
pauseLock.unlock();
}
}
}
这个实现展示了Condition在实际工程中的强大能力——它不仅仅是简单的等待/通知机制,而是构建复杂并发组件的基础模块。
