学静思语
Published on 2025-03-21 / 11 Visits
0
0

Java多线程中notify()和notifyAll()的区别

Java多线程中notify()和notifyAll()的区别

在Java多线程编程中,notify()notifyAll()是Object类提供的两个用于线程间通信的重要方法,它们在唤醒等待线程的方式上有显著的区别。下面我将详细分析这两个方法的区别。

基本定义

  • notify(): 唤醒在此对象监视器上等待的单个线程
  • notifyAll(): 唤醒在此对象监视器上等待的所有线程

核心区别

1. 唤醒线程数量

  • notify()
  • 只唤醒一个等待中的线程
  • 如果有多个线程在等待,具体唤醒哪一个是不确定的(由JVM实现决定,通常取决于线程调度器)
  • 不保证唤醒"等待时间最长"的线程
  • notifyAll()
  • 唤醒所有在该对象上等待的线程
  • 被唤醒的线程会竞争锁,但最终只有一个线程能获得锁并继续执行
  • 其余线程会重新进入锁竞争状态

2. 资源利用效率

  • notify()
  • 资源开销较小,只需唤醒一个线程
  • 适合"生产者-消费者"模式中一次只需要唤醒一个线程的场景
  • 在等待条件相同的情况下可能会造成"惊群效应"的反面问题—有线程永远得不到唤醒
  • notifyAll()
  • 资源开销较大,需要唤醒所有等待线程
  • 会导致所有线程争抢锁,可能引起上下文切换开销
  • 避免了线程饥饿问题,更加安全但效率可能较低

3. 使用场景

  • notify()适合的场景
  • 所有等待线程是同质的(做相同的事情,有相同的等待条件)
  • 每次只需要唤醒一个线程来完成特定任务
  • 资源有限,希望控制并发执行的线程数量
  • notifyAll()适合的场景
  • 等待线程是异质的(等待不同条件,执行不同任务)
  • 多个线程可能等待不同条件,但使用同一个锁对象
  • 希望避免线程饥饿问题
  • 不确定具体哪个线程应该被唤醒

代码示例

notify()示例

public class ProducerConsumer {
    private final Queue<Integer> queue = new LinkedList<>();
    private final int CAPACITY = 10;

    public void produce() throws InterruptedException {
        synchronized (queue) {
            while (queue.size() == CAPACITY) {
                queue.wait(); // 队列满了,等待消费者消费
            }

            int value = new Random().nextInt(100);
            queue.add(value);
            System.out.println("Produced: " + value);

            queue.notify(); // 只需要唤醒一个消费者线程
        }
    }

    public void consume() throws InterruptedException {
        synchronized (queue) {
            while (queue.isEmpty()) {
                queue.wait(); // 队列空了,等待生产者生产
            }

            int value = queue.poll();
            System.out.println("Consumed: " + value);

            queue.notify(); // 只需要唤醒一个生产者线程
        }
    }
}

notifyAll()示例

public class MultiConditionWaiter {
    private boolean dataLoaded = false;
    private boolean configReady = false;

    public synchronized void waitForData() throws InterruptedException {
        while (!dataLoaded) {
            wait(); // 等待数据加载
        }
        System.out.println("Data processing thread running...");
    }

    public synchronized void waitForConfig() throws InterruptedException {
        while (!configReady) {
            wait(); // 等待配置就绪
        }
        System.out.println("Configuration processing thread running...");
    }

    public synchronized void setDataLoaded() {
        dataLoaded = true;
        notifyAll(); // 唤醒所有线程,包括等待数据和等待配置的
    }

    public synchronized void setConfigReady() {
        configReady = true;
        notifyAll(); // 唤醒所有线程,包括等待数据和等待配置的
    }
}

notify()的潜在问题

  1. 线程饥饿:某些线程可能永远不会被选中唤醒,导致饥饿问题
  2. 死锁风险:如果唤醒了错误的线程(无法满足继续执行条件的线程),而没有后续notify()调用,可能导致系统死锁
  3. 不确定性:无法预测或控制哪个线程会被唤醒

notifyAll()的潜在问题

  1. 性能开销:唤醒所有线程可能导致不必要的上下文切换和锁竞争
  2. 惊群效应:所有线程被唤醒,但大多数会立即返回等待状态

最佳实践

  1. 优先使用notifyAll()
  2. 在不确定等待条件的情况下,尤其是在复杂系统中
  3. 当有多种不同条件的等待时
  4. 代码可维护性和可靠性比性能更重要时
  5. 谨慎使用notify()
  6. 性能敏感场景下
  7. 确保所有等待线程都在等待相同条件
  8. 确保任何一个被唤醒的线程都能正确处理任务
  9. 使用更高级的并发工具
  10. 使用java.util.concurrent包中的工具类(如ReentrantLockCondition)可以更精确地控制线程通信
  11. BlockingQueue等实现提供了比原始wait/notify更高级的阻塞和唤醒机制

结论

notify()notifyAll()在线程通信中各有优劣。选择使用哪一个取决于具体的应用场景、线程同步的模式以及性能要求。在多数情况下,notifyAll()是更安全的选择,尽管它可能带来一些性能开销。而在确定只需唤醒一个特定类型的线程且性能要求高的场景下,notify()可能是更好的选择。

随着Java并发编程的发展,更推荐使用java.util.concurrent包中提供的高级并发工具,它们通常比原始的wait/notify机制更安全、更高效。


Comment