Java Condition案例

wen java案例 1

Java Condition 协同机制实战破局(附完整案例)

目录导读

  1. Condition 是什么?为什么需要它?
  2. 核心 API 解析:await / signal / signalAll 的默契配合
  3. 经典案例一:多生产者-多消费者模型的精准唤醒
  4. 经典案例二:有界队列的“非满”与“非空”双条件等待
  5. Condition 与 synchronized + wait/notify 的本质区别
  6. 高频面试问答:Condition 的坑与最佳实践
  7. 性能与替代方案:何时用 Lock 而非内置锁

Condition 是什么?为什么需要它?

在 Java 并发编程中,synchronized 配合 wait() / notify() 一直是线程协作的基石,但它的局限在于:只有一个隐式条件队列,这意味着,当多个线程因不同原因等待时,notifyAll() 会唤醒所有线程,导致大量“虚假唤醒”和上下文切换开销。

Java Condition案例

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 之后的 StreamCompletableFuture 中,很多并发协作可直接使用 SemaphoreCountDownLatchCyclicBarrier 等工具,它们内部也基于 AQS(AbstractQueuedSynchronizer),但不需要手动管理 Condition。

抱歉,评论功能暂时关闭!