1. 为什么wait/notify必须放在synchronized代码块中?
这个问题困扰过很多Java开发者。我第一次在线上环境遇到IllegalMonitorStateException异常时,花了整整一个下午才搞明白原因。要理解这个设计,我们需要从线程协作的底层机制说起。
1.1 监视器锁的本质
synchronized关键字在Java中实现的是监视器锁(Monitor)机制。每个Java对象都内置了一个监视器,这个监视器包含三个关键部分:
- 互斥锁(mutex lock)
- 等待集合(wait set)
- 入口集合(entry set)
当线程调用wait()时,它必须已经持有该对象的监视器锁,因为:
wait()需要原子性地释放锁并进入等待状态- 线程被唤醒后需要重新获取锁才能继续执行
重要提示:如果在未持有锁的情况下调用
wait(),JVM无法保证这些操作的原子性,这就是抛出IllegalMonitorStateException的根本原因。
1.2 竞态条件的避免
考虑以下危险场景:
// 错误示例! if (!condition) { obj.wait(); // 这里可能错过通知 }如果没有synchronized保护:
- 线程A检查condition为false
- 在调用wait()前,线程B修改condition并调用notify()
- 线程A此时才执行wait(),导致永久等待
synchronized代码块确保了条件检查和wait()操作的原子性:
synchronized (obj) { while (!condition) { // 必须用while而不是if obj.wait(); } }1.3 JVM实现层面的要求
查看HotSpot源码(ObjectMonitor.cpp)可以看到:
void ObjectMonitor::wait(jlong millis, bool interruptible, TRAPS) { if (THREAD != _owner) { throw_illegal_monitor_state_exception(); return; } // ... }JVM会显式检查调用线程是否是锁的持有者。这个检查发生在:
wait()调用时notify()/notifyAll()调用时
2. 常见错误模式分析
2.1 未同步的wait/notify
// 反模式1:完全缺少同步 public void brokenWait() throws Exception { lock.wait(); // 直接抛出IllegalMonitorStateException }2.2 同步对象不一致
// 反模式2:锁对象与等待对象不一致 private final Object lock1 = new Object(); private final Object lock2 = new Object(); public void inconsistentLock() throws Exception { synchronized (lock1) { lock2.wait(); // 仍然抛出异常 } }2.3 条件检查缺陷
// 反模式3:使用if而不是while检查条件 synchronized (lock) { if (!condition) { lock.wait(); // 可能被虚假唤醒 } }3. 正确使用模式
3.1 标准生产者-消费者实现
class Buffer { private Queue<Integer> queue = new LinkedList<>(); private int capacity; public Buffer(int capacity) { this.capacity = capacity; } public synchronized void produce(int item) throws InterruptedException { while (queue.size() == capacity) { wait(); // 释放锁并等待 } queue.add(item); notifyAll(); // 通知消费者 } public synchronized int consume() throws InterruptedException { while (queue.isEmpty()) { wait(); } int item = queue.remove(); notifyAll(); // 通知生产者 return item; } }3.2 使用显式锁的条件变量
class ConditionExample { private final Lock lock = new ReentrantLock(); private final Condition condition = lock.newCondition(); public void await() throws InterruptedException { lock.lock(); try { condition.await(); // 相当于wait() } finally { lock.unlock(); } } public void signal() { lock.lock(); try { condition.signal(); // 相当于notify() } finally { lock.unlock(); } } }4. 深度原理分析
4.1 线程状态转换
当调用wait()时,线程状态变化如下:
- RUNNING -> BLOCKED (等待锁)
- BLOCKED -> RUNNING (获取锁)
- RUNNING -> WAITING (调用wait())
- WAITING -> BLOCKED (被notify后)
- BLOCKED -> RUNNING (重新获取锁)
4.2 虚假唤醒问题
即使没有调用notify(),线程也可能从wait()中返回。这被称为"虚假唤醒",可能由以下原因引起:
- 底层操作系统调度
- JVM实现细节
- 其他系统中断
因此必须使用while循环检查条件:
while (!condition) { wait(); }4.3 性能考量
synchronized代码块确保了:
- 内存可见性(happens-before关系)
- 操作原子性
- 线程调度有序性
5. 现代替代方案
5.1 java.util.concurrent工具类
// 使用CountDownLatch CountDownLatch latch = new CountDownLatch(1); // 线程A latch.await(); // 线程B latch.countDown();5.2 CompletableFuture
CompletableFuture<String> future = new CompletableFuture<>(); // 线程A future.thenAccept(System.out::println); // 线程B future.complete("result");5.3 消息队列模式
BlockingQueue<String> queue = new LinkedBlockingQueue<>(); // 生产者 queue.put("message"); // 消费者 String msg = queue.take();6. 最佳实践总结
- 总是使用
synchronized保护wait()/notify() - 总是使用while循环检查条件
- 优先使用
notifyAll()而非notify() - 考虑使用更高层次的并发工具
- 保持同步块尽可能短小
- 确保锁对象和等待对象一致
- 处理InterruptedException异常
在实际项目中,我遇到过因为错误使用wait/notify导致的死锁问题。通过jstack获取线程dump后,发现一个线程在WAITING状态,另一个在BLOCKED状态,最终发现是因为不同代码段使用了不同的锁对象。这个教训让我深刻理解了锁一致性的重要性。