☰
Java线程协作:wait/notify与synchronized的底层原理
2026/10/4 11:28:22 网站建设 项目流程

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

这个问题困扰过很多Java开发者。我第一次在线上环境遇到IllegalMonitorStateException异常时,花了整整一个下午才搞明白原因。要理解这个设计,我们需要从线程协作的底层机制说起。

1.1 监视器锁的本质

synchronized关键字在Java中实现的是监视器锁(Monitor)机制。每个Java对象都内置了一个监视器,这个监视器包含三个关键部分:

  • 互斥锁(mutex lock)
  • 等待集合(wait set)
  • 入口集合(entry set)

当线程调用wait()时,它必须已经持有该对象的监视器锁,因为:

  1. wait()需要原子性地释放锁并进入等待状态
  2. 线程被唤醒后需要重新获取锁才能继续执行

重要提示:如果在未持有锁的情况下调用wait(),JVM无法保证这些操作的原子性,这就是抛出IllegalMonitorStateException的根本原因。

1.2 竞态条件的避免

考虑以下危险场景:

// 错误示例! if (!condition) { obj.wait(); // 这里可能错过通知 }

如果没有synchronized保护:

  1. 线程A检查condition为false
  2. 在调用wait()前,线程B修改condition并调用notify()
  3. 线程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会显式检查调用线程是否是锁的持有者。这个检查发生在:

  1. wait()调用时
  2. 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()时,线程状态变化如下:

  1. RUNNING -> BLOCKED (等待锁)
  2. BLOCKED -> RUNNING (获取锁)
  3. RUNNING -> WAITING (调用wait())
  4. WAITING -> BLOCKED (被notify后)
  5. BLOCKED -> RUNNING (重新获取锁)

4.2 虚假唤醒问题

即使没有调用notify(),线程也可能从wait()中返回。这被称为"虚假唤醒",可能由以下原因引起:

  • 底层操作系统调度
  • JVM实现细节
  • 其他系统中断

因此必须使用while循环检查条件:

while (!condition) { wait(); }

4.3 性能考量

synchronized代码块确保了:

  1. 内存可见性(happens-before关系)
  2. 操作原子性
  3. 线程调度有序性

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. 最佳实践总结

  1. 总是使用synchronized保护wait()/notify()
  2. 总是使用while循环检查条件
  3. 优先使用notifyAll()而非notify()
  4. 考虑使用更高层次的并发工具
  5. 保持同步块尽可能短小
  6. 确保锁对象和等待对象一致
  7. 处理InterruptedException异常

在实际项目中,我遇到过因为错误使用wait/notify导致的死锁问题。通过jstack获取线程dump后,发现一个线程在WAITING状态,另一个在BLOCKED状态,最终发现是因为不同代码段使用了不同的锁对象。这个教训让我深刻理解了锁一致性的重要性。

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

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

立即咨询