Java多线程:wait()和notify()同步机制解析
1. 为什么wait()和notify()必须在同步块中这个问题困扰过不少Java开发者。我第一次在多线程项目里用wait()时IDE直接给我标了个红线警告要求必须放在synchronized块里。当时觉得编译器太死板后来踩过几次坑才明白这个设计有多精妙。简单来说这两个方法必须同步是为了解决竞态条件和丢失唤醒这两个致命问题。想象一下如果没有同步保护当线程A刚检查完条件不满足准备调用wait()时线程B可能已经修改了条件并调用了notify()这个唤醒信号就永远丢失了——这就是著名的lost wake-up问题。2. 核心机制解析2.1 对象监视器模型每个Java对象都内置一个监视器锁monitor这个锁不是通过synchronized关键字创建的而是对象与生俱来的能力。当我们调用wait()时线程会释放持有的监视器锁线程进入WAITING状态被notify()唤醒后重新竞争锁synchronized (lock) { while (!condition) { lock.wait(); // 释放锁并等待 } // 条件满足后的处理 }2.2 竞态条件的典型场景假设允许非同步调用wait()// 错误示例 if (!condition) { lock.wait(); // 这里可能错过notify }在检查condition和调用wait()之间的间隙其他线程可能已经修改了condition并调用了notify()。等当前线程真正进入等待时唤醒信号已经丢失导致永久等待。3. 正确使用模式3.1 标准模板正确的使用方式应该像这样synchronized (lock) { while (!condition) { // 必须用while而不是if lock.wait(); } // 处理业务逻辑 }关键点必须用while循环检查条件因为被唤醒时条件可能仍未满足虚假唤醒3.2 notify的使用规范对应的通知代码synchronized (lock) { condition true; lock.notifyAll(); // 或notify() }4. 底层原理剖析4.1 JVM层面的实现在HotSpot虚拟机中wait()/notify()的调用会转换为以下步骤检查当前线程是否持有对象锁未持有则抛IllegalMonitorStateException将线程加入对象的等待集(wait set)释放锁并挂起线程被唤醒后重新竞争锁4.2 等待队列与同步队列每个对象维护两个队列同步队列Entry Set竞争锁的线程等待队列Wait Set调用了wait()的线程notify()会将线程从等待队列移到同步队列notifyAll()则移动所有等待线程。5. 常见问题与避坑指南5.1 典型错误案例忘记同步// 错误 public void doWait() throws Exception { lock.wait(); // 直接抛IllegalMonitorStateException }错误的条件检查synchronized (lock) { if (!condition) { // 应该用while! lock.wait(); } }混淆锁对象private final Object lock1 new Object(); private final Object lock2 new Object(); synchronized (lock1) { lock2.wait(); // 错误锁对象不匹配 }5.2 性能优化建议优先使用notifyAll()除非明确知道只需要唤醒一个特定线程考虑使用java.util.concurrent包下的高级工具类如Condition、CountDownLatch等设置合理的wait超时时间避免永久阻塞lock.wait(3000); // 3秒超时6. 从设计模式看wait/notify这本质上是Guarded Suspension模式的实现前提条件Guardcondition条件挂起操作wait()条件变更notify()这种模式在生产者-消费者、线程池等场景广泛应用。比如线程池的任务队列为空时工作线程就会wait()直到有新任务入队时notify()。7. 现代替代方案虽然wait/notify是基础但在Java 5中更推荐使用Lock ConditionLock lock new ReentrantLock(); Condition condition lock.newCondition(); lock.lock(); try { while (!condition) { condition.await(); } } finally { lock.unlock(); }阻塞队列BlockingQueueString queue new LinkedBlockingQueue(); queue.take(); // 自动阻塞等待这些新API提供了更灵活的线程控制能力比如可中断的等待、公平锁等特性。8. 面试深度考点面试官常考察的几个深入问题为什么wait()要释放锁避免死锁如果不释放其他线程无法修改条件变量提高并发性允许其他线程获取锁执行操作wait()和sleep()的区别wait()释放锁sleep()不释放wait()是Object方法sleep()是Thread方法wait()需要被唤醒sleep()自动恢复虚假唤醒(spurious wakeup)问题即使没有notify()线程也可能从wait()返回这就是必须用while检查条件的原因9. 最佳实践总结经过多个项目的实践验证我总结出这些经验总是使用相同的锁对象进行同步和等待永远在循环中检查等待条件考虑使用超时机制避免永久阻塞优先使用notifyAll()除非有明确性能需求在复杂场景考虑使用java.util.concurrent工具类记住这些原则就能避免大多数多线程协作的问题。我在实际项目中遇到过一个典型case由于错误使用了if而不是while检查条件导致在高并发时出现概率性业务异常排查了整整两天才发现是这个问题。