wait和notify必须在synchronized中:Monitor的铁律🔐

54 阅读8分钟

为什么wait()必须在synchronized里?这不是规定,这是必然!让我们揭开Monitor锁的神秘面纱。

一、开场:一个IllegalMonitorStateException💥

代码炸弹

public class WrongWaitNotify {
    public static void main(String[] args) {
        Object lock = new Object();
        
        // ❌ 没有synchronized
        lock.wait(); // 💣 抛出IllegalMonitorStateException!
    }
}

异常信息:

Exception in thread "main" java.lang.IllegalMonitorStateException
    at java.lang.Object.wait(Native Method)

为什么会报错? 因为没有持有Monitor锁


二、什么是Monitor?🔍

Monitor对象监视器

每个Java对象都有一个Monitor(监视器),它是一种同步机制,包含:

  1. Owner(所有者):当前持有锁的线程
  2. EntryList(入口队列):等待获取锁的线程
  3. WaitSet(等待集合):调用wait()后等待的线程

可视化:

                  Monitor
    ┌─────────────────────────────────┐
    │  Owner: Thread-1                │
    ├─────────────────────────────────┤
    │  EntryList (等待锁):            │
    │    Thread-2 ──→ Thread-3        │
    ├─────────────────────────────────┤
    │  WaitSet (wait):                │
    │    Thread-4, Thread-5           │
    └─────────────────────────────────┘

Monitor的工作流程

1. 线程进入synchronized块
   → 尝试获取Monitor
   
2. 获取成功
   → 成为Owner
   → 执行代码
   
3. 获取失败
   → 进入EntryList
   → 阻塞等待
   
4. Owner释放锁
   → 唤醒EntryList中的一个线程
   → 竞争Monitor

三、为什么wait()必须在synchronized中?🤔

原因1:避免竞态条件(Race Condition)

错误示例(假设没有强制要求):

// 假设wait/notify不需要synchronized
public class BrokenProducerConsumer {
    private Queue<String> queue = new LinkedList<>();
    private Object lock = new Object();
    
    // 生产者
    public void produce(String item) {
        queue.add(item);
        lock.notify(); // ① 通知消费者
    }
    
    // 消费者
    public String consume() {
        if (queue.isEmpty()) {  // ② 检查队列
            lock.wait();        // ③ 等待
        }
        return queue.poll();    // ④ 取出元素
    }
}

时序问题:

时刻1: 消费者执行②,发现队列空
时刻2: 生产者执行①,notify()(但消费者还没wait!)
时刻3: 消费者执行③,wait()(永远等待,因为notify已经发过了!)

这就是Lost Wake-Up Problem(丢失唤醒)!

正确做法:

public class CorrectProducerConsumer {
    private Queue<String> queue = new LinkedList<>();
    
    public synchronized void produce(String item) {
        queue.add(item);
        notify(); // 在锁内notify
    }
    
    public synchronized String consume() throws InterruptedException {
        while (queue.isEmpty()) { // 循环检查
            wait(); // 在锁内wait
        }
        return queue.poll();
    }
}

为什么加synchronized就对了?

消费者:
1. 获取锁
2. 检查条件(queue.isEmpty())
3. 调用wait()
   → 原子操作:释放锁 + 进入WaitSet
   
生产者:
1. 获取锁(此时消费者已经在WaitSet中)
2. 修改状态(queue.add())
3. 调用notify()
4. 释放锁
   → 消费者被唤醒,重新竞争锁

关键: wait()会原子地释放锁并进入等待,避免了时间窗口!

原因2:保证可见性

public class VisibilityProblem {
    private boolean ready = false;
    private int data = 0;
    
    // 线程1
    public void produce() {
        data = 42;      // ①
        ready = true;   // ②
        // notify();    // ③ 假设不需要synchronized
    }
    
    // 线程2
    public void consume() {
        // while (!ready) {
        //     wait();  // ④ 假设不需要synchronized
        // }
        System.out.println(data); // ⑤ 可能看不到data=42!
    }
}

没有synchronized:

  • 线程1的写入可能不可见
  • 可能发生指令重排序

有synchronized:

  • happens-before保证可见性
  • 内存屏障防止重排序

原因3:保护共享状态

public class StateProtection {
    private int count = 0;
    private Object lock = new Object();
    
    // 假设wait不需要synchronized
    public void decrement() {
        if (count == 0) {       // ① 读count
            lock.wait();        // ② 等待
        }
        count--;                // ③ 修改count
    }
    
    public void increment() {
        count++;                // ④ 修改count
        lock.notify();          // ⑤ 通知
    }
}

问题:

  • ①和③之间,count可能被其他线程修改
  • 没有锁保护,count可能变成负数!

解决:

public synchronized void decrement() throws InterruptedException {
    while (count == 0) {
        wait(); // 释放锁后,其他线程才能修改count
    }
    count--;
}

四、wait()的完整工作流程🔄

步骤详解

synchronized (lock) {
    while (!condition) {
        lock.wait(); // ← 这里发生了什么?
    }
    // 条件满足,继续执行
}

wait()的原子操作:

1. 检查:当前线程是否持有Monitor锁
   → 没有?抛出IllegalMonitorStateException
   → 有?继续

2. 释放:释放Monitor锁
   → Owner置为null
   → 当前线程从Owner移到WaitSet

3. 阻塞:线程进入WAITING状态
   → 等待notify()/notifyAll()
   → 或者被interrupt()

4. 唤醒后:重新竞争锁
   → 从WaitSet移到EntryList
   → 获取到锁后,从wait()返回
   → 继续执行后续代码

状态转换图:

         RUNNABLE (持有锁)
              |
        调用wait()
              ↓
        WAITING (在WaitSet)
              |
        被notify()
              ↓
        BLOCKED (在EntryList)
              |
        获取到锁
              ↓
         RUNNABLE (继续执行)

五、notify()的工作流程📢

notify()做了什么?

synchronized (lock) {
    // 修改条件
    lock.notify(); // ← 唤醒一个等待线程
}

步骤:

1. 检查:当前线程是否持有Monitor锁
   → 没有?抛出IllegalMonitorStateException
   
2. 唤醒:从WaitSet中选一个线程
   → 移到EntryList
   → 线程状态:WAITING → BLOCKED
   
3. 注意:notify()不会立即释放锁!
   → 当前线程退出synchronized后才释放
   → 被唤醒的线程才能竞争锁

notify() vs notifyAll()

notify(): 唤醒一个(随机)

synchronized (lock) {
    lock.notify(); // 只唤醒一个线程
}

notifyAll(): 唤醒所有

synchronized (lock) {
    lock.notifyAll(); // 唤醒所有等待线程
}

什么时候用notifyAll?

public class MultiCondition {
    private int data = 0;
    
    // 多个条件
    public synchronized void waitForPositive() throws InterruptedException {
        while (data <= 0) {
            wait();
        }
    }
    
    public synchronized void waitForNegative() throws InterruptedException {
        while (data >= 0) {
            wait();
        }
    }
    
    public synchronized void setData(int value) {
        data = value;
        notifyAll(); // 必须用notifyAll,因为不知道该唤醒谁
    }
}

建议: 除非性能要求极高,否则优先用notifyAll(),避免信号丢失。


六、经典案例:生产者-消费者模式🏭

完整实现

public class ProducerConsumerQueue<T> {
    private final Queue<T> queue = new LinkedList<>();
    private final int capacity;
    
    public ProducerConsumerQueue(int capacity) {
        this.capacity = capacity;
    }
    
    // 生产者
    public synchronized void put(T item) throws InterruptedException {
        // 循环检查(防止虚假唤醒)
        while (queue.size() == capacity) {
            System.out.println(Thread.currentThread().getName() + " 队列满,等待...");
            wait(); // 释放锁,进入WaitSet
        }
        
        queue.add(item);
        System.out.println(Thread.currentThread().getName() + " 生产:" + item);
        
        notifyAll(); // 唤醒消费者
    }
    
    // 消费者
    public synchronized T take() throws InterruptedException {
        // 循环检查(防止虚假唤醒)
        while (queue.isEmpty()) {
            System.out.println(Thread.currentThread().getName() + " 队列空,等待...");
            wait(); // 释放锁,进入WaitSet
        }
        
        T item = queue.poll();
        System.out.println(Thread.currentThread().getName() + " 消费:" + item);
        
        notifyAll(); // 唤醒生产者
        return item;
    }
}

// 测试
public class Test {
    public static void main(String[] args) {
        ProducerConsumerQueue<Integer> queue = new ProducerConsumerQueue<>(5);
        
        // 生产者
        new Thread(() -> {
            try {
                for (int i = 0; i < 10; i++) {
                    queue.put(i);
                    Thread.sleep(100);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "生产者").start();
        
        // 消费者
        new Thread(() -> {
            try {
                for (int i = 0; i < 10; i++) {
                    queue.take();
                    Thread.sleep(200); // 消费慢
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "消费者").start();
    }
}

输出:

生产者 生产:0
生产者 生产:1
消费者 消费:0
生产者 生产:2
消费者 消费:1
生产者 生产:3
生产者 生产:4
生产者 生产:5
生产者 队列满,等待...
消费者 消费:2
生产者 生产:6
...

七、为什么必须用while而不是if?⚠️

虚假唤醒(Spurious Wakeup)

错误示例:

// ❌ 用if
public synchronized T take() throws InterruptedException {
    if (queue.isEmpty()) {
        wait();
    }
    return queue.poll(); // 可能还是空的!
}

问题场景:

1. 消费者A:检查队列空,wait()
2. 消费者B:检查队列空,wait()
3. 生产者:放入一个元素,notifyAll()
4. 消费者A:被唤醒,取走元素
5. 消费者B:被唤醒,队列又空了!
   → 执行poll(),返回null或抛异常!

正确示例:

// ✅ 用while
public synchronized T take() throws InterruptedException {
    while (queue.isEmpty()) { // 循环检查
        wait();
    }
    return queue.poll(); // 确保队列不为空
}

原因:

  • wait()醒来后,条件可能已经不满足了
  • 必须重新检查条件
  • while保证条件始终满足

八、wait() vs sleep() vs park()⏰

特性wait()sleep()LockSupport.park()
所属类ObjectThreadLockSupport
需要锁✅ 必须❌ 不需要❌ 不需要
释放锁✅ 释放❌ 不释放❌ 不释放
唤醒方式notify/notifyAll时间到/interruptunpark/interrupt
用途线程协作延时底层阻塞工具

对比示例

// wait() - 线程协作
synchronized (lock) {
    while (!condition) {
        lock.wait(); // 释放锁,等待notify
    }
}

// sleep() - 延时
Thread.sleep(1000); // 不释放锁,单纯睡眠

// park() - 底层工具
LockSupport.park(); // 阻塞当前线程
LockSupport.unpark(thread); // 唤醒指定线程

九、现代替代方案:Condition⚡

ReentrantLock + Condition

public class ModernQueue<T> {
    private final Queue<T> queue = new LinkedList<>();
    private final int capacity;
    
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();
    
    public ModernQueue(int capacity) {
        this.capacity = capacity;
    }
    
    public void put(T item) throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() == capacity) {
                notFull.await(); // 等价于wait()
            }
            queue.add(item);
            notEmpty.signal(); // 等价于notify()
        } finally {
            lock.unlock();
        }
    }
    
    public T take() throws InterruptedException {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                notEmpty.await();
            }
            T item = queue.poll();
            notFull.signal();
            return item;
        } finally {
            lock.unlock();
        }
    }
}

优势:

  • ✅ 多个Condition,精确唤醒
  • ✅ 可中断、可超时
  • ✅ 公平锁选项

十、常见陷阱与最佳实践⚠️

陷阱1:忘记循环检查

// ❌ 错误
if (condition) {
    wait();
}

// ✅ 正确
while (!condition) {
    wait();
}

陷阱2:在循环外修改共享状态

// ❌ 错误
synchronized (lock) {
    while (!ready) {
        lock.wait();
    }
}
data = processData(); // 没有锁保护!

// ✅ 正确
synchronized (lock) {
    while (!ready) {
        lock.wait();
    }
    data = processData(); // 在锁内
}

陷阱3:notify后立即修改状态

// ⚠️ 小心
synchronized (lock) {
    lock.notify();
    // 这里继续修改状态,被唤醒的线程要等待
    doSomeWork();
}

// ✅ 更好
synchronized (lock) {
    doSomeWork();
    lock.notify(); // 最后notify
}

最佳实践

  1. 总是用while检查条件
  2. wait/notify必须在synchronized中
  3. 优先用notifyAll而不是notify
  4. 避免在notify后做耗时操作
  5. 考虑用Condition替代wait/notify

十一、面试高频问答💯

Q1: 为什么wait()是Object的方法而不是Thread的?

A: 因为wait()操作的是Monitor(对象监视器),每个对象都有Monitor,所以定义在Object中。Thread的方法是操作线程本身(如sleep、interrupt)。

Q2: notify()会立即释放锁吗?

A: 不会! notify()只是把线程从WaitSet移到EntryList,当前线程退出synchronized块后才释放锁。

Q3: 为什么wait()会抛出InterruptedException?

A: 因为等待的线程可能被interrupt()中断,需要响应中断请求。

Q4: wait()和sleep()的本质区别是什么?

A:

  • wait():释放锁,用于线程协作
  • sleep():不释放锁,单纯延时

Q5: 可以不用synchronized,直接调用wait()吗?

A: 不能! 会抛出IllegalMonitorStateException。这是设计上的强制要求,保证正确性。


十二、底层实现:ObjectMonitor源码🔧

HotSpot VM中的ObjectMonitor结构(C++):

ObjectMonitor() {
    _header       = NULL;
    _count        = 0;      // 重入计数
    _waiters      = 0;      // 等待线程数
    _recursions   = 0;      // 重入次数
    _object       = NULL;
    _owner        = NULL;   // 持有锁的线程
    _WaitSet      = NULL;   // wait()的线程队列
    _EntryList    = NULL;   // 等待锁的线程队列
    _cxq          = NULL;   // 竞争队列
}

wait()的C++实现(简化):

void ObjectMonitor::wait(jlong millis, TRAPS) {
    Thread * const Self = THREAD;
    
    // 1. 保存锁状态
    intptr_t save = _recursions;
    _recursions = 0;
    
    // 2. 退出锁
    exit(true, Self);
    
    // 3. 加入WaitSet
    AddWaiter(&node);
    
    // 4. 阻塞等待
    Self->_ParkEvent->park();
    
    // 5. 被唤醒后,重新获取锁
    enter(Self);
    _recursions = save;
}

十三、总结:Monitor锁的铁律📜

核心原则:

  1. wait/notify必须在synchronized中

    • 保证原子性(避免Lost Wake-Up)
    • 保证可见性(happens-before)
    • 保护共享状态
  2. 用while而不是if

    • 防止虚假唤醒
    • 确保条件始终满足
  3. 优先用notifyAll

    • 避免信号丢失
    • 代码更健壮

记忆口诀:

synchronized保护Monitor锁, wait释放锁进WaitSet, notify唤醒竞争EntryList, while循环防止虚假醒。


下期预告: 线程中断机制:如何优雅地停止线程?interrupt()的正确姿势!🛑