并发 - Condition 精确等待与唤醒

并发编程 —— Condition 精确等待与唤醒

1. 核心理论:为什么需要 Condition?

我们已经学会了使用锁(Synchronized / ReentrantLock)来保证线程安全,但仅有锁是不够的。在很多复杂的业务场景下,线程之间需要互相通信和协作。

问题场景(生产者-消费者模式, Producer-Consumer pattern):

  • 一个生产者线程向一个共享仓库中添加产品。
  • 一个消费者线程从仓库中取出产品。
  • 协作需求:
    1. 如果仓库满了,生产者必须停止生产并等待 (stop and wait),直到有空位。
    2. 如果仓库空了,消费者必须停止消费并等待 (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 (“不空”) 的条件。
  • 协作流程:

    1. 厨师想放菜,先锁住出餐口 (lock.lock())。
    2. 他发现出餐台是满的。于是,他在 notFull 这个条件下开始等待 (notFull.await()),并自动让出了出餐口的锁。
    3. 服务员想取菜,也先锁住出餐口。他成功了,因为厨师已经让出了锁。
    4. 服务员从出餐台取走菜品,然后他通知 (notFull.signal()) 那些正在 notFull 条件下等待的厨师:“嘿,现在有空位了!”
    5. 服务员解锁出餐口。
    6. 被唤醒的厨师重新去竞争锁,成功后,从 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();
    }
}
posted @ 2026-01-21 16:03  我是刘瘦瘦  阅读(40)  评论(0)    收藏  举报