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 是随机唤醒吗?",你就知道他其实想听的是 规范与实现的区别 ,以及锁竞争对执行顺序的最终影响。把这篇文章的逻辑理清,这个知识点你就彻底通关了。

相关推荐
文丑颜不良啊1 小时前
LeetCode201~225题目解析
java·leetcode
SL_staff3 小时前
4G设备掉线三层归因指南:SIM卡、模组、JVS-IOT网关配置精准锁定
java·物联网·产品
Wang's Blog4 小时前
Java 项目实战: 交友项目 - 黑名单:取消黑名单
java·windows·交友
杨杨杨大侠4 小时前
DeepSeek Harness:连 Agent Loop 都能替换的插件架构
java·人工智能·agent
SL_staff4 小时前
私有化低代码运维三重雷区:JVS开源栈中的Kafka/ES/RabbitMQ治理实践
java·低代码·全栈
yuniko-n4 小时前
【算法】约瑟夫环问题
java·算法
郝亚军4 小时前
manifest 中 `sha256` 与 `signValue` 的作用
java·开发语言·数据结构
譕痕5 小时前
stripe 实例
java·开发语言·stripe·checksession
墨子夕5 小时前
设计模式——Java × Python 双语言精讲 · 23 个经典模式 + 现代演进(前言篇)
java·开发语言·设计模式
骇客野人5 小时前
Linux 全套实操方案(含目录详解、文件传输、日志、文件规范、环境变量、Java 应用部署运维)
java·linux·运维