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

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

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

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

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

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

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

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

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

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

相关推荐
Java牛马1 小时前
Caffeine 缓存及其应用相关总结
java·redis·缓存·caffeine·数据一致性·本地缓存
一只叫煤球的猫2 小时前
Spring AI 2.0 源码解析(三):ChatClient 的 Fluent API 如何构建请求?
java·后端·面试
软件开发JR2 小时前
基于Web的足球青训俱乐部管理后台系统的设计与开发
java·前端·spring boot·毕业设计
重生之我是Java开发战士2 小时前
【Java EE】Spring AOP :面向切面编程
java·spring·java-ee
凤山老林2 小时前
基于 Spring Batch 的海量数据迁移与批处理架构:分片、容错与断点续跑
java·spring boot·spring·架构·spring batch
Javatutouhouduan2 小时前
Java初学者如何高效学习JVM?
java·jvm·java虚拟机·java面试·后端开发·java程序员·java八股文
ZJU_统一阿萨姆3 小时前
【算子开发】全局内存访问与合并访存
java·服务器·网络·人工智能·语言模型
7177773 小时前
不止工具集成:基于 Gitee 软件工厂构建 DevSecOps 研发治理底座
java·服务器·gitee
gis开发之家3 小时前
Spring Boot 4 深度解析——参数接收大全:@RequestParam、@PathVariable、@RequestBody
java·spring boot·后端·spring