1. 线程通信的基本原理
在多线程编程中,wait()和notify()是Java提供的线程间通信机制。这两个方法都定义在Object类中,这意味着任何Java对象都可以作为线程通信的载体。理解它们的工作机制,首先要明白Java对象头中的监视器(Monitor)概念。
每个Java对象都有一个内置锁(intrinsic lock)和等待队列。当线程调用对象的wait()方法时,它会释放锁并进入等待状态;notify()则用于唤醒等待队列中的线程。这种设计源于早期操作系统的管程(Monitor)概念,是解决并发问题的经典模式。
关键点:wait()和notify()本质上是基于对象监视器的底层操作,它们直接与对象的锁机制交互。
2. 同步块的必要性解析
2.1 竞态条件的预防
假设没有同步块保护,考虑以下场景:
- 线程A检查条件不满足,准备调用wait()
- 线程B此时修改了条件并调用notify()
- 线程A才真正执行wait()
这种情况下,线程A将永远等待,因为错过了唤醒信号。同步块通过原子性地执行"检查条件-执行等待"这一系列操作,避免了这种竞态条件。
java复制// 错误示例:无同步保护
if (!condition) {
obj.wait(); // 可能永远等待
}
// 正确用法
synchronized(obj) {
while (!condition) {
obj.wait();
}
}
2.2 锁状态的强制要求
从JVM实现层面看,wait()必须知道当前线程持有哪个对象的锁。当线程调用wait()时:
- JVM需要确认线程确实持有该对象的锁
- 需要记录锁的释放和后续重新获取
- 需要将线程放入正确的等待队列
这些操作都依赖于明确的同步上下文。如果没有同步块,JVM无法确定操作的目标锁对象,也无法保证操作的原子性。
3. 底层机制深度剖析
3.1 对象监视器结构
每个Java对象在内存中的布局包含:
- 对象头(Mark Word):存储锁状态、GC信息等
- 类元数据指针
- 实例数据
- 对齐填充
其中对象头中的锁标志位决定了对象的锁状态。wait()和notify()操作会修改这些标志位,这要求调用线程必须已经获得锁。
3.2 线程状态转换流程
当调用wait()时,线程经历以下状态变化:
- 从RUNNING变为WAITING
- 释放对象锁
- 进入等待队列
- 被notify()唤醒后,重新竞争锁
- 获得锁后从wait()返回
这个复杂的状态转换过程必须在一个原子操作中完成,否则会导致锁状态不一致。
4. 常见误用与正确模式
4.1 典型错误案例
- 丢失唤醒问题:
java复制// 线程A
if (queue.isEmpty()) {
queue.wait(); // 可能永远阻塞
}
// 线程B
queue.add(item);
queue.notify();
- 虚假唤醒问题:
java复制synchronized(obj) {
if (!condition) { // 应该用while而不是if
obj.wait();
}
}
4.2 最佳实践模板
正确的使用模式应包含三个要素:
- 同步块保护
- 循环条件检查
- 异常处理
java复制synchronized(lockObject) {
while (!condition) {
try {
lockObject.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
// 处理中断逻辑
}
}
// 执行条件满足后的操作
}
5. JVM规范与实现细节
5.1 语言规范要求
Java语言规范明确规定了这些限制:
- wait()/notify()必须在同步块中调用
- 调用线程必须是锁的持有者
- 违反规则会抛出IllegalMonitorStateException
这种设计不是偶然的,而是经过深思熟虑的并发安全方案。
5.2 HotSpot实现示例
在OpenJDK的HotSpot虚拟机中,相关实现位于:
- ObjectMonitor.cpp:处理监视器操作
- ObjectSynchronizer.cpp:实现锁机制
关键代码片段:
cpp复制void ObjectMonitor::wait(jlong millis, bool interruptible, TRAPS) {
// 检查当前线程是否持有锁
if (THREAD != _owner) {
THROW_MSG(vmSymbols::java_lang_IllegalMonitorStateException(),
"current thread not owner");
}
// ...后续等待逻辑
}
6. 替代方案与高级用法
6.1 java.util.concurrent工具类
现代Java开发推荐使用更高级的并发工具:
- Lock/Condition接口
- CountDownLatch
- CyclicBarrier
- BlockingQueue
这些工具提供了更灵活的线程控制,例如:
java复制Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
lock.lock();
try {
while (!conditionMet) {
condition.await();
}
// 处理业务
} finally {
lock.unlock();
}
6.2 特定场景优化
- 超时控制:
java复制synchronized(obj) {
long remaining = timeout;
while (!condition && remaining > 0) {
remaining = obj.wait(remaining);
}
}
- 批量唤醒:
java复制synchronized(obj) {
obj.notifyAll(); // 唤醒所有等待线程
}
7. 性能考量与调优建议
7.1 锁粒度优化
过度同步会导致性能问题。优化建议:
- 减小同步块范围
- 使用不同的锁对象分离不相关的操作
- 考虑读写锁(ReadWriteLock)替代独占锁
7.2 上下文切换开销
wait()/notify()会导致线程状态切换,建议:
- 避免频繁的等待/唤醒
- 考虑使用自旋等待(针对短时间等待)
- 使用线程池管理工作者线程
8. 跨平台一致性保障
Java的内存模型确保了这些操作在不同平台上的行为一致:
- 写操作对读操作的可见性
- 操作的有序性
- 原子性保证
这使得wait()/notify()机制在各种硬件架构上都能正确工作,这是同步块要求的另一个重要原因。