Java 并发灵魂拷问:notify/signal 唤醒顺序是规范还是实现?

Java 并发面试里,一道看似简单的问题常能刷掉 90% 的人:

Object.notify() 和 Condition.signal() 唤醒线程的顺序是什么?随机还是 FIFO?

今天我们就从 JVM 规范、HotSpot 源码、AQS 设计原理三个层面,一次把这件事说透。

结论

  1. Object.notify()(synchronized 配套)

    • JVM 规范:没有规定顺序,允许任意选择。
    • JDK8 HotSpot 实现 :从 WaitSet 头部取线程,表现为 FIFO,但这只是"实现上的巧合"。
    • 工程红线 :严禁依赖该顺序 ,因为 notify 唤醒后线程会进入 EntryList 重新竞争锁,而 synchronized 是非公平锁,最终执行顺序无法保证。
  2. Condition.signal()(AQS / ReentrantLock 配套)

    • 严格按 FIFO 顺序唤醒,唤醒的是等待队列头部线程。
    • 不是随机,而且是设计约定,业务可以合理依赖。

一、synchronized + wait() / notify():WaitSet 里的秘密

synchronized 的重量级锁底层依赖 ObjectMonitor,它内部有两个关键队列:

  • EntryList:等待获取锁的线程队列(竞争锁)。
  • WaitSet :调用了 wait() 后释放锁并挂起的线程队列(单向链表)。

notify() 到底怎么选线程?

从 JDK8 HotSpot 源码 看,notify() 会从 WaitSet 中取出链表头部的第一个等待线程 进行唤醒,因此肉眼观测到的现象是先入先出(FIFO)。

但是! 这恰恰就是 90% 的人掉坑的地方。

误区 ①:既然 HotSpot 是 FIFO,业务代码就能依赖这个顺序?

绝对不能。 原因有二:

  1. 这是 HotSpot 内部实现细节,不是规范。

    Object.notify() 的 Javadoc 写得很清楚:

    The choice is arbitrary and occurs at the discretion of the implementation.

    任意 JVM 都有权实现成随机、优先级优先,明天 HotSpot 改成随机也完全合法。

  2. 唤醒顺序 FIFO ≠ 执行顺序 FIFO。

    notify 唤醒的线程不会立刻拿到锁,它会从 WaitSet 移入 EntryList ,再次参与 monitor 竞争。而 synchronized 采用的是非公平锁策略,新来的活跃线程完全可以"插队"先抢到锁。

    举个例子:

    线程 A 先 wait,线程 B 后 wait。notify 确实按 FIFO 先唤醒了 A,可就在 A 准备拿锁的瞬间,外部线程 C 闯进来一把抢走了锁------A 继续阻塞。最终执行的顺序依然是"无序"的。

误区 ②:notify 到底是不是"随机唤醒"?

这是全网最大的争议点。正确说法是:

  • 规范层面 :允许随机,我们应把它当作随机来设计程序。
  • HotSpot 实现层面:JDK8 是 FIFO,JDK 高版本、带特定 JVM 参数、有中断/伪唤醒等场景可能打破这个现象。
  • OpenJ9 等其它虚拟机:完全可能是不同的策略。

结论:不要试图从 notify 的结果反推顺序,写并发代码必须假设它是随机的。

二、Lock + Condition.await() / signal():精心设计的 FIFO

如果你需要有顺序保证 的线程唤醒,应该用 java.util.concurrent.locks 下的 Lock 和 Condition,底层是 AQS。

底层载体:Condition Queue(双向链表,严格 FIFO)

Condition 内部维护了一个独立的等待队列,由 firstWaiter 和 lastWaiter 指针维护。

  • await():线程释放锁,然后被包装成节点,插入等待队列尾部。
  • signal():直接取出 firstWaiter(队首节点) 进行唤醒,这是写在源码里的 FIFO。
  • signalAll():从队首开始,逐个唤醒队列中所有节点。

signal() 的唤醒顺序是设计目标,不是实现巧合,可以放心依赖。

但依然要记住:

即使 signal 按 FIFO 精准唤醒,被唤醒的线程仍然要回到 AQS 同步队列重新争锁 。如果锁本身是非公平的(默认 ReentrantLock 可设为公平/非公平),最终的执行顺序仍可能受锁竞争影响 ,但至少唤醒这个动作是 100% 有序的。

三、一张图看懂区别

API 所属体系 唤醒顺序 能否依赖
Object.notify() synchronized JVM 规范无要求;HotSpot 实现为 FIFO(取 WaitSet 头部),但唤醒后需竞争非公平锁,最终乱序 严禁依赖
Condition.signal() AQS / Lock 固定从 Condition 队列头部唤醒,严格 FIFO 可合理依赖

四、细节

1. 虚假唤醒与中断

即使 notify 按 FIFO 唤醒,wait() 也可能被虚假唤醒或中断打断。这会使 WaitSet 内部的链表结构发生变化,你肉眼看到的"有序"可能瞬间消失。

2. notifyAll 不是"一起跑"

notifyAll 会把所有线程从 WaitSet 移到 EntryList,但它们还是一个一个去抢锁,执行顺序依然由锁竞争决定。

3. 不要和 signal 的设计哲学搞混

  • Object.notify() 设计出来只是"随便叫醒一个",官方从没承诺过顺序。
  • Condition.signal() 设计出来就是为了在等待队列中精准按序激活 ,这是 Lock 体系相较 synchronized 的一大优势。

五、极简总结

  • notify :规范允许乱序,HotSpot 碰巧写成有序,属于实现彩蛋,不能当饭吃。
  • signal :设计目标就是有序,这是它存在的理由。

理解了这一层,下次面试官再问"notify 是随机唤醒吗?",你就知道他其实想听的是 规范与实现的区别 ,以及锁竞争对执行顺序的最终影响。把这篇文章的逻辑理清,这个知识点你就彻底通关了。

相关推荐
知守观11 分钟前
OBS 同名文件互相覆盖,客户拿错了别人的报告:一次文件串号的排查与重构
java·后端
泡海椒1 小时前
JQuick-Excel 字段映射实战:用 MAPPING 固化 Excel 表头与业务字段契约
xml·java·开发语言·excel
梦幻通灵1 小时前
IDEA 实用快捷键【持续更新】
java·ide·intellij-idea
南归北隐1 小时前
Spring AI Alibaba Graph框架实现Tools工具调用
java·后端·spring·spring ai·spring ai tools
开开心心就好1 小时前
视频模糊怎么修复?免费工具支持批量处理
java·前端·人工智能·智能手机·pdf·excel
w_zero_one1 小时前
链表(3)
java·数据结构·算法
资深技术分享员2 小时前
遗留系统——把“改不动的老系统“接过来
java·服务器·数据库
江湖有缘2 小时前
Docker实战 | 使用Docker部署Bibliotheca阅读习惯管理工具
java·docker·容器
艾莉丝努力练剑2 小时前
【AI大模型接入SDK】ChatSDK:CMake构建静态库完整实现
java·开发语言·网络·c++·人工智能·学习·sdk
百度一下吧2 小时前
umi后台管理项目实战:从工程搭建到生产构建
java·前端·javascript