游乐游手机版
首页/编程语言/文章详情

怎么通过 Object 类的 wait/notify 机制在面向对象层面实现初级的线程间协作

时间:2026-05-03 06:05
怎么通过 Object 类的 wait notify 机制在面向对象层面实现初级的线程间协作 在Ja va多线程编程里,让线程之间有序协作,而不是乱成一锅粥,是个基本功。Object 类提供的 wait() 和 notify()(以及它的兄弟 notifyAll())方法,正是实现这种“等待-唤醒”

怎么通过 Object 类的 wait/notify 机制在面向对象层面实现初级的线程间协作

怎么通过 Object 类的 wait/notify 机制在面向对象层面实现初级的线程间协作

在Ja va多线程编程里,让线程之间有序协作,而不是乱成一锅粥,是个基本功。Object 类提供的 wait()notify()(以及它的兄弟 notifyAll())方法,正是实现这种“等待-唤醒”契约的核心工具。简单来说,就是让一个线程在条件不满足时先“歇着”,等另一个线程准备好后再把它“叫醒”。

但这里有个铁律,必须牢记:所有对 waitnotify 的调用,都必须老老实实地放在 synchronized 同步块里,而且锁对象,必须就是调用这些方法的那个对象本身。这是整个机制能安全运转的基石。

共享状态对象作为协作枢纽

怎么理解这个“契约”呢?关键在于,要有一个共享的状态对象。你可以定义一个普通的Ja va类,比如叫 TaskQueue,用它来封装需要协作的业务状态——比如任务列表是不是空了,或者计算结果有没有准备好。这个对象身兼三职:它是数据的载体,是同步用的锁,更是线程间通信的“信号灯”。

  • 线程A想调用 queue.wait() 去等待,前提是它必须先拿到 queue 这把锁(也就是进入 synchronized(queue) 块)。一旦调用 wait(),它就会释放锁,然后乖乖进入等待队列,直到被唤醒。
  • 线程B在修改了共享状态(比如往队列里加了新任务)之后,同样需要在 synchronized(queue) 块里,调用 queue.notify()queue.notifyAll() 来发出信号。
  • 被唤醒的线程(比如刚才的线程A)会重新加入竞争,去抢 queue 的锁。抢到之后,它才能从 wait() 的地方继续执行。这里有个至关重要的细节:醒来后第一件事,应该是重新检查等待条件是否真的满足了。

必须配合 while 循环做条件重检

为什么必须重新检查?因为 wait() 存在“虚假唤醒”的可能性——线程可能因为某些与业务逻辑无关的原因(比如操作系统信号干扰)而被唤醒。所以,绝对不能简单地用 if 判断一次就了事,必须用 while 循环把等待条件包起来:

synchronized (sharedObj) {
  while (!conditionMet()) { // 用while,不是if!
    sharedObj.wait();
  }
  // 执行到这里时,conditionMet() 肯定为 true了,可以安全地处理后续逻辑
}

举个典型的例子,在生产者-消费者模型里,消费者线程能行动的前提是“队列非空”。所以,它的等待逻辑必须是 while(!queue.isEmpty()) wait(); —— 即使被唤醒了,也得再瞅一眼队列,确认真的有东西可消费才行。

notify 与 notifyAll 的选择依据

那么,叫醒一个线程,是用 notify() 还是 notifyAll() 呢?这得看场景。

  • notify() 会随机唤醒等待在同一个对象上的一个线程。它适合条件单一、等待方角色明确的场景,比如两个线程严格交替执行。
  • notifyAll() 则更“豪爽”,它会唤醒所有正在等待的线程。这种方式更安全、更通用,尤其是在多个线程等待不同条件(比如有的在等“非空”,有的在等“未满”)的复杂场景下。唤醒后,让每个线程自己用 while 循环去判断是否满足了自己的条件,满足的就继续执行,不满足的继续等待。

对于初学者来说,有个实用的建议:如果不确定该用哪个,那就统一使用 notifyAll()。虽然可能带来一点点性能开销,但它能有效避免因唤醒错对象而导致的死锁风险。当然,如果你能百分之百确定,只有一类线程在等待某个条件,并且你一旦唤醒它,条件肯定满足,那么用 notify() 会更轻量一些。

典型协作模式示例:简单阻塞队列

理论说了这么多,来看一个最经典的“生产一个,消费一个”的协作模式,思路就非常清晰了:

  • 生产者线程的行动路线是:获取锁 → 检查队列是否已满(用while循环判断)→ 如果满了就 wait() → 添加元素 → 调用 notifyAll() → 释放锁。
  • 消费者线程的行动路线是:获取锁 → 检查队列是否为空(同样用while循环判断)→ 如果空了就 wait() → 取出元素 → 调用 notifyAll() → 释放锁。

这个模式里的关键点有两个:一是双方都在修改完共享状态后、离开同步块前调用 notifyAll();二是双方都坚持用 while 循环来重新检查等待条件。把握住这两点,一个基础的线程协作框架就搭建起来了。

来源:https://www.php.cn/faq/2411171.html
上一篇怎么在 Java 中使用 Logger 记录不同级别的系统运行日志 下一篇如何在 Java 中通过 ArrayList.remove() 根据索引或对象删除集合中的元素
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
如何在ThinkPHP中实现定时任务与命令行调度方法
编程语言 · 2026-07-04

如何在ThinkPHP中实现定时任务与命令行调度方法

用ThinkPHP实现定时任务时,很多开发者第一步就卡在命令行报错上,直接输入php think your:command却无法识别——这种情况绝大多数是因为命令类的注册方式存在问题。下面先梳理几个核心要点。 ThinkPHP 6 中 think 命令如何正确触发自定义指令 直接运行 php thi

ThinkPHP API接口防重放攻击实现方法
编程语言 · 2026-07-04

ThinkPHP API接口防重放攻击实现方法

先说几个核心判断:API防重放攻击这件事,做对了是道防火墙,做错了就是个心理安慰。很多开发者到踩坑了才明白——验签这东西,放错位置、漏掉字段、存错nonce,每一环都能让整个安全体系直接归零。 验签必须放在中间件里,不能在控制器里写 ThinkPHP 的请求生命周期中,中间件是唯一能在路由匹配、参数

ThinkPHP文件上传必须验证扩展名安全必要性分析
编程语言 · 2026-07-04

ThinkPHP文件上传必须验证扩展名安全必要性分析

在使用ThinkPHP进行文件上传时,ext扩展名验证通常是开发者首先接触的关键环节。但你真的了解它的实际工作原理吗?它仅比对文件名后缀,而不读取文件内容,甚至对空格和大小写都极其敏感。更为重要的是——它是TP文件上传验证五层防线中不可忽视的第一道关卡,一旦配置遗漏,整个validate验证链将直接

ThinkPHP关联模型自动写入与更新使用教程
编程语言 · 2026-07-04

ThinkPHP关联模型自动写入与更新使用教程

需要明确的是,ThinkPHP关联模型并没有提供所谓的“自动写入 更新”魔法开关。所谓的“自动”功能,实际上都需要开发者手动编写配置逻辑才能生效。核心原则在于:主模型和从模型必须分开独立处理,时间戳字段和业务字段需依靠修改器或钩子接管;批量操作则要规规矩矩地绕过模型逻辑来执行——只有理解透彻这些要点

BoxLayout中仅居中一个组件其他默认左对齐
编程语言 · 2026-07-04

BoxLayout中仅居中一个组件其他默认左对齐

在 Java Swing 中使用 BoxLayout 的 Y_AXIS 方向布局时,很多初学者容易掉进一个常见陷阱:希望将某个组件单独设置为中心对齐,但当调用 `setAlignmentX(CENTER_ALIGNMENT)` 后,却发现其他组件也跟着发生了偏移,完全达不到预期效果。实际上,关键之处