广告

Java 线程通信全解析:从 wait/notify/notifyAll 到 Condition(Lock)的实战应用与最佳实践

1. Java 线程通信的基础概念

1.1 线程通信的重要性与模型

线程之间的协作 是多线程程序中实现正确行为的关键,而 wait/notify/notifyAll 则构成了 Java 语言层面的基本工具,用于在对象的监视器上实现等待、唤醒和条件判断。互斥与可见性是该模型的核心保障。本文聚焦于 Java 线程通信全解析:从 wait/notify/notifyAll 到 Condition(Lock)的实战应用与最佳实践,并逐步揭示不同实现的优劣与适用场景。

通过监视锁,Java 确保同一时刻只有一个线程进入临界区,其他进入等待队列的线程在被唤醒后重新竞争锁。正确的条件判断与循环等待模式是避免“伪唤醒”的关键。下面的示例将直观地展示等待与唤醒的基本用法。

public class WaitNotifyExample {
    private final Object lock = new Object();
    private boolean ready = false;

    public void waitForReady() throws InterruptedException {
        synchronized (lock) {
            while (!ready) {
                lock.wait(); // 放弃锁并进入等待
            }
            // 继续执行
        }
    }

    public void setReady() {
        synchronized (lock) {
            ready = true;
            lock.notifyAll(); // 通知等待线程
        }
    }
}

2. wait/notify/notifyAll 的工作机制与陷阱

2.1 基本用法与正确范式

在使用 waitnotifynotifyAll 时,必须在同一个 同步块或方法中对同一个对象的锁进行操作。循环检查条件是避免伪唤醒的重要设计。

notify 会唤醒等待队列中的一个线程,而 notifyAll 会唤醒所有等待线程,但具体谁获得 CPU 时段由调度器决定。为了避免死锁和错位唤醒,通常使用 while 而非 if 进行条件判断。

public class WaitNotifyDemo {
    private final Object lock = new Object();
    private boolean dataAvailable = false;

    public void producer() {
        synchronized (lock) {
            dataAvailable = true;
            lock.notifyAll();
        }
    }

    public void consumer() {
        synchronized (lock) {
            while (!dataAvailable) {
                try {
                    lock.wait();
                } catch (InterruptedException ignored) {}
            }
            // 处理数据
        }
    }
}

3. 使用 Lock 与 Condition 替代 wait/notify

3.1 Condition 的语义与用法

Lock 提供了更灵活的锁获取策略和可重入特性,而 Condition 则像 对象的等待集合,为不同的等待条件提供独立的条件变量。

使用 ReentrantLockCondition 可以避免一些与 synchronized 相关的陷阱,并且可以实现更细粒度的等待策略。每个 Condition 都有自己的等待队列,唤醒策略更可控。

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

public class ConditionDemo {
    private final Lock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();
    private final int[] buffer = new int[10];
    private int count = 0;

    public void put(int value) throws InterruptedException {
        lock.lock();
        try {
            while (count == buffer.length) {
                notFull.await();
            }
            buffer[count++] = value;
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public int take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                notEmpty.await();
            }
            int val = buffer[--count];
            notFull.signal();
            return val;
        } finally {
            lock.unlock();
        }
    }
}

4. 实战案例:生产者-消费者模型的实现

4.1 基于 Condition 的生产者-消费者

生产者-消费者是并发编程中的经典场景,使用 Condition 可以让生产者在缓冲区满时等待,并在有空间时被唤醒;消费者在缓冲区为空时等待,并在有数据时被唤醒。通过将不同条件分解为独立的变量,可以避免“错位唤醒”和竞争语义的混乱。

在高并发场景中,锁的粒度和条件变量的数量直接影响吞吐量与响应时间。合理设计队列边界和唤醒策略,有助于降低 上下文切换成本,提升整体性能。

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

public class ProducerConsumer {
    private final Queue queue = new LinkedList<>();
    private final int maxSize = 100;
    private final Lock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();

    public void produce(int value) throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() == maxSize) {
                notFull.await();
            }
            queue.offer(value);
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public int consume() throws InterruptedException {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                notEmpty.await();
            }
            int val = queue.poll();
            notFull.signal();
            return val;
        } finally {
            lock.unlock();
        }
    }
}

5. 最佳实践与性能考量

5.1 设计原则与常见坑

在设计 Java 线程通信时,尽量降低锁粒度,使用 独立的条件变量来分离不同的等待条件,可以减少不必要的唤醒。

另外,避免在锁持有期间执行耗时操作,以减少竞争和上下文切换;对于等待超时的场景,优先使用带有超时参数的等待方法,以免“永久阻塞”线程。

广告

后端开发标签