并发 - Condition 精确等待与唤醒
并发编程 —— Condition 精确等待与唤醒
1. 核心理论:为什么需要 Condition?
我们已经学会了使用锁(Synchronized / ReentrantLock)来保证线程安全,但仅有锁是不够的。在很多复杂的业务场景下,线程之间需要互相通信和协作。
问题场景(生产者-消费者模式, Producer-Consumer pattern):
- 一个生产者线程向一个共享仓库中添加产品。
- 一个消费者线程从仓库中取出产品。
- 协作需求:
- 如果仓库满了,生产者必须停止生产并等待 (stop and wait),直到有空位。
- 如果仓库空了,消费者必须停止消费并等待 (stop and wait),直到有新产品。
如果只用锁,线程在获取不到资源时只能不停地循环检查(自旋),这会极大地浪费 CPU 资源。为了解决这个问题,我们需要一种机制,让线程可以在不满足条件时高效地挂起等待 (efficiently suspend and wait),并在条件满足时被其他线程精确地唤醒 (precisely awakened)。
Condition 接口就是为此而生的。它通常被称为条件队列 (condition queue) 或条件变量 (condition variable)。
- 核心思想:
Condition必须依附于一个Lock对象。它将传统的对象监视器方法(wait,notify,notifyAll)分解成不同的对象,从而可以与任意Lock实现组合使用。更重要的是,一个Lock可以关联多个Condition对象,实现对不同条件的精确控制。
2. 深度剖析:Condition 的核心方法
Condition 接口的核心方法与 Object 的 wait/notify 非常相似,但功能更强。
-
await(): 使当前线程进入等待状态,直到被唤醒或被中断。在调用await()时,线程会自动释放它所持有的Lock,以便其他线程可以获取锁并改变条件。当线程被唤醒后,它会重新尝试获取锁,获取成功后才会从await()方法返回。 -
signal(): 唤醒一个正在此Condition上等待的线程。JVM 会从该Condition的等待队列中选择一个线程来唤醒。被唤醒的线程并不会立即执行,而是需要先重新获取锁。 -
signalAll(): 唤醒所有正在此Condition上等待的线程。
使用范式: await, signal, signalAll 方法都必须在持有与该 Condition 关联的 Lock 的情况下调用,否则会抛出 IllegalMonitorStateException(非法监视器状态异常)。标准的写法如下:
lock.lock();
try {
while (/* 条件不满足 */) {
condition.await(); // 等待,并自动释放锁
}
// 执行条件满足后的业务逻辑
} finally {
lock.unlock();
}
注意:检查等待条件时,必须使用 while 循环,而不是 if。这是为了防止“虚假唤醒 (Spurious Wakeup)”,即线程在没有被 signal 的情况下意外唤醒。使用 while 可以确保线程被唤醒后,再次检查条件是否真的满足。
3. 生活中的例子与代码示例:餐厅的厨师与服务员
-
生活比喻: 想象一家餐厅的后厨与前台。
Lock: 整个出餐口的锁。同一时间只能有一个人(厨师或服务员)在操作出餐口。- 共享资源: 出餐台,假设最多只能放 1 道菜。
Condition(厨师侧): 一个名为notFull(“未满”) 的条件。Condition(服务员侧): 一个名为notEmpty(“不空”) 的条件。
-
协作流程:
- 厨师想放菜,先锁住出餐口 (
lock.lock())。 - 他发现出餐台是满的。于是,他在
notFull这个条件下开始等待 (notFull.await()),并自动让出了出餐口的锁。 - 服务员想取菜,也先锁住出餐口。他成功了,因为厨师已经让出了锁。
- 服务员从出餐台取走菜品,然后他通知 (
notFull.signal()) 那些正在notFull条件下等待的厨师:“嘿,现在有空位了!” - 服务员解锁出餐口。
- 被唤醒的厨师重新去竞争锁,成功后,从
await()中返回,再次检查发现出餐台确实空了,于是把新菜放上去。
- 厨师想放菜,先锁住出餐口 (
-
核心代码示例: 使用
ReentrantLock和两个Condition实现一个有界队列。
package com.study.concurrency;
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;
class BoundedBuffer {
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition(); // 生产者等待的条件
private final Condition notEmpty = lock.newCondition(); // 消费者等待的条件
private final Queue<Object> queue = new LinkedList<>();
private final int capacity;
public BoundedBuffer(int capacity) {
this.capacity = capacity;
}
public void put(Object item) throws InterruptedException {
lock.lock();
try {
// 1. 如果队列满了,生产者在 notFull 条件上等待
while (queue.size() == capacity) {
System.out.println("队列已满,生产者 " + Thread.currentThread().getName() + " 等待...");
notFull.await();
}
queue.add(item);
System.out.println("生产者 " + Thread.currentThread().getName() + " 生产了: " + item);
// 2. 通知可能在等待的消费者
notEmpty.signal();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
// 3. 如果队列空了,消费者在 notEmpty 条件上等待
while (queue.isEmpty()) {
System.out.println("队列为空,消费者 " + Thread.currentThread().getName() + " 等待...");
notEmpty.await();
}
Object item = queue.remove();
System.out.println("消费者 " + Thread.currentThread().getName() + " 消费了: " + item);
// 4. 通知可能在等待的生产者
notFull.signal();
return item;
} finally {
lock.unlock();
}
}
}
public class ConditionDemo {
public static void main(String[] args) {
BoundedBuffer buffer = new BoundedBuffer(5);
// 生产者线程
Thread producer = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
buffer.put("item-" + i);
Thread.sleep(100);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "Producer");
// 消费者线程
Thread consumer = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
buffer.take();
Thread.sleep(500);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "Consumer");
producer.start();
consumer.start();
}
}

浙公网安备 33010602011771号