1. 并发编程中的等待-通知机制本质
当多个线程需要协同完成某项任务时,单纯的忙等待(busy-waiting)会浪费CPU资源。以生产者-消费者模型为例,如果消费者线程通过循环检查队列是否为空的方式获取数据,会导致CPU占用率居高不下。等待-通知机制通过线程阻塞与唤醒的协作方式解决了这个问题。
Java中的等待-通知机制实现包含三个关键操作:
wait():释放锁并进入WAITING状态notify():随机唤醒一个等待线程notifyAll():唤醒所有等待线程
重要提示:这三个方法必须在使用synchronized获取对象锁的代码块内调用,否则会抛出IllegalMonitorStateException
1.1 对象监视器模型
每个Java对象都关联着一个监视器(Monitor),这个监视器维护着:
- 一个互斥锁(mutex lock)
- 一个等待集合(wait set)
- 一个入口集合(entry set)
当线程调用wait()时:
- 释放对象锁
- 线程状态变为WAITING
- 线程进入该对象的等待集合
当其他线程调用notify()时:
- JVM从等待集合中随机选择一个线程
- 被选中的线程从WAITING状态变为BLOCKED状态
- 线程移入入口集合,等待获取锁
2. 标准等待-通知范式实现
2.1 基础模板代码
// 共享对象 class SharedObject { // 条件谓词 private boolean condition = false; public synchronized void waitForCondition() throws InterruptedException { // 必须使用循环检查条件 while (!condition) { wait(); } // 条件满足后的处理逻辑 doSomething(); } public synchronized void changeCondition() { condition = true; notifyAll(); // 或 notify() } }2.2 关键实现要点
条件检查必须使用while循环:
- 防止虚假唤醒(spurious wakeup)
- 确保条件真正满足后才继续执行
notify()与notifyAll()的选择:
- notify()效率更高但风险更大
- notifyAll()更安全但可能引起"惊群效应"
- 通常建议优先使用notifyAll()
锁对象的选取原则:
- 使用专门的对象作为锁(private final Object)
- 避免使用业务对象或this作为锁
3. 生产级应用实践
3.1 带超时的等待实现
public synchronized boolean waitForCondition(long timeout) throws InterruptedException { long endTime = System.currentTimeMillis() + timeout; while (!condition) { long remaining = endTime - System.currentTimeMillis(); if (remaining <= 0) { return false; // 超时未满足 } wait(remaining); } return true; }3.2 多条件谓词处理
class MultiCondition { private final Object lock = new Object(); private boolean conditionA = false; private boolean conditionB = false; public void waitForA() throws InterruptedException { synchronized (lock) { while (!conditionA) { lock.wait(); } } } public void signalA() { synchronized (lock) { conditionA = true; lock.notifyAll(); } } // 类似实现conditionB... }4. 典型问题排查指南
4.1 常见问题现象
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| IllegalMonitorStateException | 未获取锁就调用wait/notify | 确保在synchronized块内调用 |
| 线程永久阻塞 | 忘记调用notify | 检查所有条件变更路径 |
| 虚假唤醒 | 使用if检查条件 | 改用while循环检查 |
| 性能问题 | 过度使用notifyAll | 评估是否可用notify替代 |
4.2 死锁场景分析
等待-通知机制使用不当可能导致死锁:
- 线程A持有锁L1,等待条件C1
- 线程B持有锁L2,等待条件C2
- C1的满足需要线程B释放L2
- C2的满足需要线程A释放L1
排查技巧:使用jstack获取线程转储,检查各线程持有的锁和等待的锁
5. 性能优化实践
5.1 减少锁竞争
- 缩小同步代码块范围
- 使用读写锁(ReentrantReadWriteLock)替代互斥锁
- 考虑使用条件队列(Condition)替代基本等待通知
5.2 避免过早唤醒
// 优化前 public synchronized void process() { notifyAll(); // 不必要地唤醒所有线程 // 长时间处理... } // 优化后 public void process() { // 先完成非同步处理 doLongTimeWork(); synchronized(this) { notifyAll(); // 最后才通知 } }6. 高级应用场景
6.1 连接池实现示例
class ConnectionPool { private final LinkedList<Connection> pool = new LinkedList<>(); private final int maxSize; public ConnectionPool(int maxSize) { this.maxSize = maxSize; } public Connection get() throws InterruptedException { synchronized (pool) { while (pool.isEmpty()) { pool.wait(); } return pool.removeFirst(); } } public void release(Connection conn) { synchronized (pool) { pool.addLast(conn); pool.notify(); // 只需唤醒一个等待线程 } } }6.2 批量任务处理
class BatchProcessor { private int count = 0; private final int batchSize; public BatchProcessor(int batchSize) { this.batchSize = batchSize; } public synchronized void await() throws InterruptedException { count++; if (count < batchSize) { wait(); } else { count = 0; notifyAll(); // 唤醒所有等待线程 } } }在实际项目中,等待-通知机制的性能表现会受JVM实现影响。以Oracle HotSpot JVM为例,notify()的平均唤醒延迟通常在微秒级别,但在高并发场景下可能上升到毫秒级。对于超低延迟要求的系统,可以考虑使用Java 9引入的VarHandle或sun.misc.Unsafe(不推荐)提供的更底层API