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()的潜在问题
- 线程饥饿:某些线程可能永远不会被选中唤醒,导致饥饿问题
- 死锁风险:如果唤醒了错误的线程(无法满足继续执行条件的线程),而没有后续notify()调用,可能导致系统死锁
- 不确定性:无法预测或控制哪个线程会被唤醒
notifyAll()的潜在问题
- 性能开销:唤醒所有线程可能导致不必要的上下文切换和锁竞争
- 惊群效应:所有线程被唤醒,但大多数会立即返回等待状态
最佳实践
- 优先使用notifyAll():
- 在不确定等待条件的情况下,尤其是在复杂系统中
- 当有多种不同条件的等待时
- 代码可维护性和可靠性比性能更重要时
- 谨慎使用notify():
- 性能敏感场景下
- 确保所有等待线程都在等待相同条件
- 确保任何一个被唤醒的线程都能正确处理任务
- 使用更高级的并发工具:
- 使用
java.util.concurrent
包中的工具类(如ReentrantLock
和Condition
)可以更精确地控制线程通信 BlockingQueue
等实现提供了比原始wait/notify更高级的阻塞和唤醒机制
结论
notify()
和notifyAll()
在线程通信中各有优劣。选择使用哪一个取决于具体的应用场景、线程同步的模式以及性能要求。在多数情况下,notifyAll()
是更安全的选择,尽管它可能带来一些性能开销。而在确定只需唤醒一个特定类型的线程且性能要求高的场景下,notify()
可能是更好的选择。
随着Java并发编程的发展,更推荐使用java.util.concurrent
包中提供的高级并发工具,它们通常比原始的wait/notify机制更安全、更高效。