Java并发编程之Wait/Notify

一、Monitor对象的核心结构

每个Java对象都关联一个Monitor对象,其核心字段包括:

  1. owner:指向当前持有锁的线程。
  2. EntryList:存放竞争锁的线程队列(阻塞状态)。
  3. WaitSet:存放调用wait()后释放锁的线程队列(等待状态)。

二、wait()notify()的执行逻辑

1. wait()方法的作用
  • 释放锁:当前线程释放对象的Monitor锁。
  • 进入等待队列:线程被放入对象的WaitSet队列中(等待被唤醒)。
  • 挂起线程:线程进入WAITINGTIMED_WAITING状态,直到被notify()唤醒或超时。
2. notify()方法的作用
  • 唤醒线程:从WaitSet中随机唤醒一个线程,将其移动到EntryList队列中(重新竞争锁)。
  • 不释放锁:调用notify()的线程不会立即释放锁,需退出同步块后锁才释放。
3. notifyAll()方法的作用
  • 唤醒WaitSet中所有等待线程,将它们全部移动到EntryList

三、关键规则与注意事项

  1. 必须在同步块内调用
    wait()/notify()必须持有对象的Monitor锁,否则抛出IllegalMonitorStateException

  2. 条件检查必须用while而非if
    避免虚假唤醒(Spurious Wakeup),即使未被通知,线程也可能从wait()返回。例如:

    synchronized (lock) {
        while (条件不满足) { // 必须用while循环检查条件
            lock.wait();
        }
        // 执行操作
    }
    
  3. 唤醒策略

    • notify():随机唤醒一个线程,适合单生产者-单消费者场景。
    • notifyAll():唤醒所有线程,确保不会遗漏等待线程(更安全,但可能增加竞争)。
  4. 锁释放与重入

    • wait()会释放锁,唤醒后需重新竞争锁才能继续执行。
    • notify()不会释放锁,线程需退出同步块才会释放锁。

四、执行流程图解

生产者线程:
1. 获取锁 → 检查条件(消息是否已消费)
   ↓ 条件满足
2. 生产消息 → 设置empty=false → 调用notifyAll()
3. 退出同步块 → 释放锁

消费者线程:
1. 获取锁 → 检查条件(消息是否未消费)
   ↓ 条件满足
2. 消费消息 → 设置empty=true → 调用notifyAll()
3. 退出同步块 → 释放锁

等待线程(WaitSet):
1. 被notifyAll()唤醒 → 移动到EntryList
2. 重新竞争锁 → 获取锁后继续执行

六、常见问题

1. 为什么wait()notify()属于Object类?
  • 因为它们依赖对象的Monitor锁,而每个对象都可作为锁载体。
2. 与sleep()的区别?
  • sleep():不释放锁,线程暂停指定时间。
  • wait():释放锁,线程进入等待状态直到被唤醒。
3. 如何避免死锁?
  • 确保所有wait()都有对应的notify()唤醒。
  • 避免嵌套锁或长时间持有锁。

七、总结

通过wait()/notify()配合Monitor锁,可以实现线程间的精准协作:

  • wait():释放锁并等待条件满足。
  • notify():通知等待线程条件已变化。
    正确使用这两个方法需严格遵循同步规则和条件检查,避免线程饥饿或死锁问题。

拓展:wait与sleep

1. 所属类与调用方式

方法 所属类 调用方式
wait() Object 必须在同步块中调用(需持有对象锁)
sleep() Thread 静态方法,可直接调用(无需持有锁)

2. 锁的释放行为

方法 是否释放锁? 锁释放时机
wait() 释放锁 调用wait()后立即释放锁,线程进入等待状态
sleep() 不释放锁 线程休眠期间一直持有锁,其他线程无法进入同步块

3. 唤醒机制

方法 唤醒方式 中断响应
wait() 需其他线程调用notify()/notifyAll() 响应中断(抛出InterruptedException
sleep() 超时后自动唤醒,或通过interrupt()中断 响应中断(抛出InterruptedException

4. 关键区别总结

特性 wait() sleep()
锁释放 立即释放锁 不释放锁
调用条件 必须在同步块中调用 任意位置调用
唤醒方式 依赖notify()/notifyAll() 超时或中断唤醒
用途 线程间协作(依赖共享资源条件) 单纯暂停线程执行

5. 常见误区

  1. 在非同步块中调用wait()
    会导致 IllegalMonitorStateException

  2. sleep()代替wait()
    可能导致死锁(线程休眠期间不释放锁,其他线程无法获取锁)。

  3. 虚假唤醒(Spurious Wakeup)
    wait()返回后必须用while循环重新检查条件,而非if

  4. 中断处理
    两者均需捕获InterruptedException,但wait()唤醒后需重新竞争锁。

你可能感兴趣的:(Java并发编程,java,开发语言)