Java多线程:wait()和notify()为何需synchronized?
2026/9/19 4:56:00 网站建设 项目流程

1. 为什么wait()和notify()必须放在synchronized代码块中?

这个问题困扰过很多刚接触Java多线程开发的程序员。我第一次在实际项目中遇到IllegalMonitorStateException异常时也是一头雾水——明明代码逻辑看起来没问题,为什么一运行就报错?后来通过深入研究才发现,这背后蕴含着Java线程同步机制的精妙设计。

1.1 监视器(Monitor)的基本原理

每个Java对象都有一个内置的监视器(Monitor),这是实现线程同步的基础。可以把监视器想象成一个房间的门锁:

  • synchronized关键字就像是获取这个门锁的钥匙
  • wait()相当于暂时离开房间并交出钥匙
  • notify()像是敲门告诉里面的人可以出来了

关键点在于:你必须有钥匙(synchronized)才能进行这些操作。如果没有获取锁就直接调用wait()或notify(),就像没有钥匙却想进出房间,自然会抛出IllegalMonitorStateException

1.2 丢失唤醒(Lost Wakeup)问题

这是最核心的原因。让我们通过一个典型的生产者-消费者场景来说明:

// 错误示例 if (!hasData) { // 步骤1:检查条件 // 这里可能发生线程切换! lock.wait(); // 步骤2:等待 }

假设消费者线程执行完步骤1但还未执行步骤2时,CPU切换到生产者线程:

  1. 生产者设置hasData=true并调用notify()
  2. 然后消费者线程继续执行wait()

结果就是生产者发出的通知被"丢失"了,消费者将永远等待下去。这种问题在实际多线程环境中非常危险且难以调试。

关键点:synchronized保证了"条件检查"和"等待"这两个操作成为一个原子操作,不会被其他线程打断。

1.3 虚假唤醒(Spurious Wakeup)的应对

即使使用synchronized,线程也可能在没有收到明确通知的情况下被唤醒,这就是所谓的"虚假唤醒"。因此我们总是使用while循环而不是if语句来检查条件:

// 正确做法 while (!condition) { lock.wait(); }

synchronized在这里的作用是确保在检查条件和调用wait()期间,共享状态不会被其他线程修改。

2. wait()和notify()的底层机制

2.1 wait()的三步操作

很多人以为wait()只是让线程暂停,实际上它完成了三个关键操作:

  1. 释放当前持有的锁
  2. 将线程加入该对象的等待队列
  3. 线程进入WAITING状态

这与sleep()有本质区别:sleep()不会释放任何锁资源。

2.2 notify()的工作机制

notify()并不会立即唤醒线程,而是:

  1. 从等待队列中移出一个线程
  2. 将该线程放入入口队列(Entry Set)
  3. 这个线程需要重新竞争锁才能继续执行

notifyAll()则是唤醒所有等待线程,它们会一起竞争锁。

2.3 监视器的状态转换

理解对象监视器的状态转换对掌握多线程编程至关重要:

获取锁(synchronized) ↓ 运行状态 ↓ wait() → 等待队列 ↑ notify() → 入口队列 ↓ 重新竞争锁

这个状态转换图解释了为什么必须在持有锁时才能调用这些方法——它们直接操作监视器的内部队列。

3. 实际应用中的最佳实践

3.1 标准模板代码

经过多年实践,我总结出使用wait/notify的安全模板:

// 等待方 synchronized(lock) { while (!condition) { lock.wait(); } // 处理业务 } // 通知方 synchronized(lock) { // 改变条件 condition = true; lock.notifyAll(); }

3.2 为什么更推荐notifyAll()

虽然notify()更高效,但在实际项目中我更推荐使用notifyAll(),原因包括:

  1. 避免复杂的线程调度问题
  2. 防止某些线程被"遗忘"在等待队列中
  3. 代码更健壮,特别是在条件可能被多个因素改变时

3.3 性能优化技巧

在高并发场景下,可以采取以下优化措施:

  1. 尽量减少同步块内的代码量
  2. 使用超时版本的wait(long timeout)
  3. 考虑使用更高级的并发工具如Condition

4. 常见问题与调试技巧

4.1 典型错误案例

我在代码审查中经常看到这些错误:

  1. 在非同步方法中调用wait/notify
  2. 使用if而不是while检查条件
  3. 忘记在改变条件后调用notify
  4. 对不同的条件使用同一个锁对象

4.2 调试多线程问题的技巧

调试多线程问题确实很有挑战性,我常用的方法包括:

  1. 给线程设置有意义的名称
  2. 使用Thread.dumpStack()定位问题
  3. 在关键点添加详细的日志
  4. 使用jstack工具分析线程状态

4.3 替代方案的选择

对于现代Java开发,我们有了更多选择:

  1. java.util.concurrent包中的高级工具
  2. Lock和Condition接口
  3. 并发集合类
  4. CompletableFuture等异步编程工具

但在理解这些高级工具之前,掌握基础的wait/notify机制仍然非常重要。

5. 从JVM角度看实现原理

5.1 对象头中的Mark Word

每个Java对象头中都包含Mark Word,其中存储了锁状态信息:

  • 偏向锁标志
  • 轻量级锁指针
  • 重量级锁指针(指向Monitor对象)

wait/notify操作的就是这个重量级锁关联的Monitor。

5.2 MonitorObject的结构

HotSpot JVM中MonitorObject包含:

  1. _owner:指向持有锁的线程
  2. _EntryList:等待获取锁的线程队列
  3. _WaitSet:调用了wait()的线程队列

这解释了为什么必须先获取锁才能操作等待队列。

5.3 本地方法实现

最终wait/notify是通过JNI调用实现的:

// ObjectMonitor.cpp void ObjectMonitor::wait(jlong millis, bool interruptible, TRAPS) { // 将线程加入_WaitSet // 释放锁 // 等待唤醒 }

理解这些底层实现有助于我们写出更健壮的多线程代码。

6. 历史演进与设计哲学

6.1 早期Java的线程模型

Java从1.0版本就引入了synchronized和wait/notify机制,这种设计:

  1. 借鉴了Hoare的监视器概念
  2. 与当时的硬件条件相适应
  3. 提供了基础的线程通信能力

6.2 为什么这样设计

这种看似严格的限制实际上体现了Java的安全哲学:

  1. 强制开发者考虑线程安全问题
  2. 避免隐晦的并发bug
  3. 提供确定性的行为

6.3 现代并发工具的关系

后来的并发工具如ReentrantLock并没有取代这种机制,而是:

  1. 提供了更灵活的锁操作
  2. 支持多个条件变量
  3. 增加了可中断、可定时等功能

但wait/notify作为基础机制仍然重要。

7. 实际项目经验分享

7.1 电商库存管理案例

我曾在一个电商系统中使用wait/notify实现库存预警:

class Inventory { private int stock; private final Object lock = new Object(); public void consume() { synchronized(lock) { while (stock <= 0) { lock.wait(); } stock--; } } public void replenish(int amount) { synchronized(lock) { stock += amount; lock.notifyAll(); } } }

这个实现简单但可靠,处理了高峰期的大量并发请求。

7.2 性能调优经验

在高并发场景下,我们需要注意:

  1. 同步块的范围要尽可能小
  2. 考虑使用读写锁分离场景
  3. 监控等待线程的数量
  4. 设置合理的超时时间

7.3 遇到的坑与解决方案

记忆最深的一个bug是:

  • 问题:系统偶尔会挂起
  • 原因:某个异常路径跳过了notify调用
  • 解决:在finally块中确保调用notify

这个教训让我养成了在finally中处理锁的好习惯。

8. 扩展知识与相关概念

8.1 与Condition的对比

Java 5引入的Condition接口提供了更灵活的功能:

  1. 一个锁可以关联多个Condition
  2. 支持公平/非公平选择
  3. 提供更丰富的等待方法

但在简单场景下,内置的wait/notify仍然是最轻量的选择。

8.2 与其他语言的比较

不同语言的线程同步机制各有特点:

  • C++: std::condition_variable
  • Python: threading.Condition
  • Go: channel-based同步

Java的设计在安全性和性能之间取得了良好平衡。

8.3 响应式编程的影响

随着响应式编程的兴起,传统的线程同步模式正在被观察者模式等替代,但理解这些基础仍然重要。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询