深度分析Java线程同步之使用Condition

在多线程的世界里,协调不同线程之间的工作是确保数据一致性和程序正确性的基石。传统的synchronized关键字配合Objectwait()notify()notifyAll()方法虽然基础,但其“一个锁对应一个等待队列”的模型显得过于粗放,无法满足更精细化的线程调度需求。

一、为何需要Condition?

synchronized的监视器模型存在一个明显缺陷:当持有锁的线程需要等待某个特定条件(如“缓冲区非满”或“缓冲区非空”)时,它只能进入一个统一的等待集合。当条件满足时,我们无法精确地唤醒那些只等待特定条件的线程(例如只唤醒生产者或只唤醒消费者),只能通过notifyAll()唤醒所有等待线程,让它们去竞争锁并重新检查条件。这会导致大量的上下文切换和锁竞争,性能低下且容易产生“惊群效应”。

Condition接口的出现,正是为了解决这一问题。它将一个锁(Lock)关联了多个等待条件,允许线程为不同的条件在不同的集合中等待,从而实现更精确、更高效的线程间通信。

二、Condition核心机制

Condition对象是由Lock对象调用newCondition()方法创建的。你可以将其理解为一个绑定在特定锁上的“条件队列”。

其核心方法与传统方法对应,但更清晰、安全:

  • await() -> wait(): 释放当前占有的锁,并在此条件上等待。
  • signal() -> notify(): 唤醒在此条件上等待的一个线程。
  • signalAll() -> notifyAll(): 唤醒在此条件上等待的所有线程。

关键优势:

  1. 精细通知:一个锁可以创建多个Condition实例。例如,可以为“非空”和“非满”各创建一个条件队列。当缓冲区为空时,消费者在“非空”条件上等待;当生产者放入数据后,只需signal“非空”条件队列上的一个消费者,而不会错误地唤醒其他生产者线程。
  2. 可中断与超时:Condition提供了更丰富的等待方法,如awaitUninterruptibly()(不可中断等待)、awaitNanos(long nanosTimeout)(超时等待),灵活性远超传统方式。
  3. 公平性:与ReentrantLock的公平锁策略配合使用时,Condition的等待线程唤醒顺序可以遵循公平原则。
三、实战示例:生产者-消费者模型

下面是一个使用ReentrantLock和两个Condition实现的有界缓冲区生产者-消费者模型。它高效地解决了“非空”和“非满”两个条件的等待与通知问题。

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class BoundedBufferWithCondition {
    final Lock lock = new ReentrantLock();
    final Condition notFull = lock.newCondition();  // “非满”条件
    final Condition notEmpty = lock.newCondition(); // “非空”条件

    final Object[] items = new Object[10]; // 容量为10的缓冲区
    int putptr, takeptr, count; // 放入索引、取出索引、当前数量

    // 生产者方法:放入数据
    public void put(Object x) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length) { // 1. 如果缓冲区满了
                notFull.await();           //   就在“非满”条件上等待
            }
            items[putptr] = x;
            if (++putptr == items.length) putptr = 0; // 环形队列
            ++count;
            notEmpty.signal();             // 2. 放入成功后,通知消费者“非空”了
        } finally {
            lock.unlock();
        }
    }

    // 消费者方法:取出数据
    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {           // 1. 如果缓冲区空了
                notEmpty.await();         //   就在“非空”条件上等待
            }
            Object x = items[takeptr];
            if (++takeptr == items.length) takeptr = 0; // 环形队列
            --count;
            notFull.signal();              // 2. 取出成功后,通知生产者“非满”了
            return x;
        } finally {
            lock.unlock();
        }
    }
}
四、深度分析
  1. while循环检查条件:无论是await()前还是被唤醒后,都必须用while循环重新检查条件。因为被唤醒时,条件可能再次变得不满足(例如,另一个消费者抢先取走了数据)。
  2. 信号的选择:示例中使用了signal()而非signalAll()。这是一种优化,因为它只唤醒一个线程,减少了竞争。在大多数情况下,这已经足够,因为每次puttake操作只会改变一个单位的状态,只需要唤醒一个对应的线程。如果一次操作可以满足多个等待线程的条件(例如,putN操作放入多个数据),则可以考虑使用signalAll()
  3. 锁的释放与重新获取:调用await()会原子地释放锁并进入等待。当被signal()唤醒后,线程在从await()返回前会重新获取锁,保证了线程安全。
结论

Condition接口将传统的对象监视器方法(wait, notify, notifyAll)分解为 distinct objects(不同的条件对象),通过将锁与多个等待集组合使用,提供了比synchronized更精细、更灵活、性能更高的线程协作手段。对于构建复杂的并发组件(如ArrayBlockingQueue等JUC集合),它是不可或缺的核心工具。掌握Condition,意味着你的并发编程能力从“能用”迈向了“精通”的新高度。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐