当前位置: 首页 > news >正文

java并发编程系列——waitnotify的正确使用姿势


核心概念:为什么需要 waitnotify

在多线程环境中,线程之间有时需要协作,而不是完全独立地运行。一个典型的场景就是生产者-消费者模型

  • 生产者线程:负责生产数据(比如往一个列表里添加元素)。
  • 消费者线程:负责消费数据(比如从同一个列表里取出元素)。
    问题来了:
  1. 如果生产者速度太快,列表满了,生产者就应该停下来等待,直到消费者消费了数据,列表有了空位。
  2. 如果消费者速度太快,列表空了,消费者就应该停下来等待,直到生产者生产了新数据。
    wait()notify()/notifyAll() 就是Java提供的用于实现这种线程间协作(或称条件等待)的底层机制。它们必须与 synchronized 关键字配合使用,因为它们操作的是对象的监视器锁

一、使用方法详解

wait(), notify(), notifyAll() 都是 java.lang.Object 类中的 final 方法,这意味着任何Java对象都可以作为锁,并拥有这些方法。

1. wait()

当一个线程调用某个对象的 wait() 方法时,会发生以下几件事:

  1. 释放锁:当前线程会立即释放它持有的该对象的监视器锁。这一点至关重要,否则其他线程就无法进入同步块来唤醒它。
  2. 进入等待状态:当前线程会进入该对象的等待集中,处于阻塞状态,不会消耗CPU资源。
  3. 等待被唤醒:线程会一直等待,直到以下四种情况之一发生:
    • 其他线程调用了同一个对象notify()notifyAll() 方法。
    • 其他线程调用了该线程的 interrupt() 方法,导致线程中断并抛出 InterruptedException
    • 如果调用的是带超时参数的 wait(long timeout),超过了指定的毫秒数。
    • 发生了所谓的“虚假唤醒”,即线程在没有被任何线程显式通知的情况下自己醒来了。
      重要:线程被唤醒后,它不会立即执行。它需要重新尝试获取之前释放的监视器锁。成功获取锁后,它会从 wait() 方法调用的地方继续向下执行
2. notify()

当一个线程调用某个对象的 notify() 方法时:

  1. 它会从该对象的等待集中随机唤醒一个正在等待的线程。
  2. 被唤醒的线程进入就绪状态,开始与其他线程竞争该对象的监视器锁。
  3. 注意:调用 notify() 的线程不会立即释放锁。它会在当前 synchronized 块执行完毕后,才会释放锁。因此,被唤醒的线程需要等待锁被释放后才能继续执行。
3. notifyAll()

当一个线程调用某个对象的 notifyAll() 方法时:

  1. 它会唤醒该对象等待集中所有正在等待的线程。
  2. 所有被唤醒的线程都进入就绪状态,共同竞争这个对象的监视器锁。
  3. 最终只有一个线程能成功获取锁并继续执行,其他线程则重新进入等待状态。

二、经典示例:生产者-消费者模型

下面是一个使用 wait/notifyAll 实现的简单生产者-消费者模型。

import java.util.LinkedList;
import java.util.Queue;
public class ProducerConsumerExample {// 共享资源,作为锁对象private static final Queue<String> buffer = new LinkedList<>();private static final int CAPACITY = 5;public static void main(String[] args) {// 创建生产者和消费者线程Thread producerThread = new Thread(new Producer(), "Producer-Thread");Thread consumerThread = new Thread(new Consumer(), "Consumer-Thread");producerThread.start();consumerThread.start();}// 生产者任务static class Producer implements Runnable {@Overridepublic void run() {int item = 0;while (true) {try {produce(item++);Thread.sleep(1000); // 模拟生产耗时} catch (InterruptedException e) {Thread.currentThread().interrupt();}}}private void produce(int item) throws InterruptedException {// 必须在 synchronized 块内synchronized (buffer) {// 【最佳实践1】使用 while 循环检查条件while (buffer.size() == CAPACITY) {System.out.println("缓冲区已满,生产者等待...");buffer.wait(); // 缓冲区满,等待}// 生产数据buffer.add("Item-" + item);System.out.println(Thread.currentThread().getName() + " 生产了: Item-" + item + ", 缓冲区大小: " + buffer.size());// 【最佳实践2】状态改变后,通知所有等待的线程buffer.notifyAll(); }}}// 消费者任务static class Consumer implements Runnable {@Overridepublic void run() {while (true) {try {consume();Thread.sleep(1500); // 模拟消费耗时} catch (InterruptedException e) {Thread.currentThread().interrupt();}}}private void consume() throws InterruptedException {// 必须在 synchronized 块内synchronized (buffer) {// 【最佳实践1】使用 while 循环检查条件while (buffer.isEmpty()) {System.out.println("缓冲区为空,消费者等待...");buffer.wait(); // 缓冲区空,等待}// 消费数据String item = buffer.poll();System.out.println(Thread.currentThread().getName() + " 消费了: " + item + ", 缓冲区大小: " + buffer.size());// 【最佳实践2】状态改变后,通知所有等待的线程buffer.notifyAll();}}}
}

三、最佳实践与常见陷阱

这是 wait/notify 的精髓所在,遵循这些规则可以避免绝大多数并发问题。

1. 【黄金法则】永远在 while 循环里调用 wait()

错误做法

synchronized (lock) {if (condition_is_not_met) { // 错误!应该用 whilelock.wait();}// do something
}

正确做法

synchronized (lock) {while (condition_is_not_met) { // 正确!lock.wait();}// do something
}

为什么必须用 while

  • 防止虚假唤醒:JVM允许线程在没有收到 notify 的情况下从 wait() 中醒来。如果是 if 判断,线程会错误地继续执行,而此时条件可能并不满足。while 循环会让线程被唤醒后重新检查条件。
  • 处理多个等待线程:假设一个生产者 notifyAll(),唤醒了另一个生产者和一个消费者。如果被唤醒的是生产者,它发现缓冲区仍然是满的(因为消费者还没来得及消费),while 循环会让它再次进入 wait() 状态。如果是 if,它就会错误地尝试向满的缓冲区添加数据。
2. 必须在 synchronized 块或方法中调用

调用 wait(), notify(), notifyAll() 的线程必须持有该对象的监视器锁。否则,会抛出 IllegalMonitorStateException 运行时异常。

// 错误示例
public void badMethod() {lock.notify(); // 抛出 IllegalMonitorStateException
}
// 正确示例
public void goodMethod() {synchronized (lock) {lock.notify(); // 正确}
}
3. 优先使用 notifyAll(),而非 notify()

notify() 只随机唤醒一个线程,这很危险。继续上面的生产者-消费者例子:

  • 缓冲区满了,一个生产者在等待。
  • 消费者消费了一个数据后,调用 notify()
  • 不幸的是notify() 可能会唤醒另一个正在等待的生产者,而不是那个消费者。被唤醒的生产者发现缓冲区还是满的,于是再次 wait()。而真正的消费者却因为没有被通知而继续等待,可能导致死锁
    notifyAll() 唤醒所有线程,虽然看起来开销大一点,但它更安全。所有被唤醒的线程都会通过 while 循环重新检查自己的运行条件,确保只有真正符合条件的线程才能继续执行。在99%的场景下,notifyAll() 都是比 notify() 更好的选择。
4. 在状态改变之后才调用 notify()

通知的目的是告诉其他线程“状态变了,快来看看你是否可以继续了”。所以,必须先完成状态的修改(比如往 buffer 里添加或删除元素),然后再调用 notifyAll()

synchronized (buffer) {// 1. 先修改状态buffer.add(item);// 2. 再通知其他线程buffer.notifyAll(); 
}

四、现代Java的替代方案

wait()/notify() 是Java提供的底层同步机制,虽然强大,但使用起来非常容易出错。从Java 5开始,java.util.concurrent 包提供了更高级、更安全、更易于使用的并发工具,强烈推荐在新代码中使用。在后续系列中我会做出讲解。

场景wait/notify 实现现代替代方案优势
生产者-消费者synchronized + wait/notifyAllBlockingQueue (如 ArrayBlockingQueue, LinkedBlockingQueue)代码极简,无需手动处理锁和等待,内置线程安全。
多条件等待多个 synchronized 块 + notifyAll (逻辑复杂)Lock + Condition一个锁可以关联多个Condition,实现精确唤醒特定类型的线程,避免了notifyAll带来的无效竞争。
资源计数synchronized + 计数器 + wait/notifyAllSemaphore语义清晰,API简单,用于控制同时访问特定资源的线程数量。
线程等待汇合join()wait/notifyAllCountDownLatch一次性同步工具,一个或多个线程等待其他一组线程完成操作后再执行。
循环栅栏复杂的 wait/notifyAll 逻辑CyclicBarrier让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,所有被阻塞的线程才会继续执行。可重用。
LockCondition 示例:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
// 使用 Lock 和 Condition 的生产者消费者
public class ProducerConsumerWithLock {private final Queue<String> buffer = new LinkedList<>();private final int CAPACITY = 5;private final Lock lock = new ReentrantLock();// 为生产者和消费者分别创建条件private final Condition notFull = lock.newCondition();private final Condition notEmpty = lock.newCondition();// ... 生产者和消费者的逻辑 ...public void produce(String item) throws InterruptedException {lock.lock();try {while (buffer.size() == CAPACITY) {notFull.await(); // 在 notFull 条件上等待}buffer.add(item);System.out.println("Produced: " + item);notEmpty.signal(); // 唤醒一个在 notEmpty 条件上等待的消费者} finally {lock.unlock();}}public String consume() throws InterruptedException {lock.lock();try {while (buffer.isEmpty()) {notEmpty.await(); // 在 notEmpty 条件上等待}String item = buffer.poll();System.out.println("Consumed: " + item);notFull.signal(); // 唤醒一个在 notFull 条件上等待的生产者return item;} finally {lock.unlock();}}
}

在这个例子中,signal() 精确地唤醒了对方类型的线程,效率比 notifyAll() 更高。

总结

特性wait() / notify()java.util.concurrent 工具
易用性低,规则复杂,极易出错高,API设计符合直觉,封装了底层细节
安全性低,需开发者严格遵守最佳实践高,由并发大师设计,经过充分测试
灵活性低,一个锁只有一个等待集高,如Lock可绑定多个Condition
性能在简单场景下开销小在复杂场景下性能更好,减少了不必要的线程唤醒和竞争
最终建议
  • 学习 wait/notify:理解它对于掌握Java并发底层原理至关重要,尤其是在阅读和维护一些老旧的库或框架代码时。
  • 使用 java.util.concurrent:在编写新的并发代码时,优先选择 java.util.concurrent 包下的高级工具。它们能让你的代码更健壮、更简洁、更易于维护。

http://www.dtcms.com/a/615292.html

相关文章:

  • 【ros2】ROS2功能包(Package)完全指南
  • 南昌网站建设渠道seo优化方案案例
  • 温州建设局网站首页网络推广一个月工资多少
  • MYSQL聚合函数
  • 做搜狗手机网站优化网站开发专业的领军人物
  • python 做网站缺点外贸都有哪些平台
  • 服装定制网站的设计与实现俄罗斯搜索引擎yandex
  • 做网站的专业公司wordpress onetone
  • 用网上的文章做网站行吗微网站 合同
  • jEasyUI 树形网格惰性加载节点
  • 我的读书清单
  • 群晖可以做网站服务器网站建设策划执行
  • 学校响应式网站模板全球最大的设计网站
  • 网站建设公司潍坊怎么注册网站免费的
  • 这么做网站教程wordpress标签使用文章列表
  • 快速上手 Dart 基础
  • 免费网站建网页优化包括什么
  • 国外有哪些网站可以做电商网站建设教程简笔画
  • DINOv3 无监督训练自定义数据集预处理技术详解 (ImageNet 兼容格式)
  • 35网站建设网站建设服务好公司排名
  • 微商网站制作百度商家
  • LeetCode 分类刷题:2487. 从链表中移除节点
  • spring1
  • 注册网站地址中国建筑一局
  • 视频剪辑教程自学网站为什么用php做网站
  • 百度统计怎么添加网站设置方法
  • C++98 标准详解:C++的首次标准化
  • 哪家专门做特卖的网站阳谷网站建设电话
  • RFSOC配置QSPI+EMMC启动 petalinux记录
  • Win11右键菜单如何把“显示更多选项“中的内容改为默认展示出来