Java 并发面试里,一道看似简单的问题常能刷掉 90% 的人:
Object.notify()和Condition.signal()唤醒线程的顺序是什么?随机还是 FIFO?今天我们就从 JVM 规范、HotSpot 源码、AQS 设计原理三个层面,一次把这件事说透。
结论
-
Object.notify()(synchronized配套)- JVM 规范:没有规定顺序,允许任意选择。
- JDK8 HotSpot 实现 :从
WaitSet头部取线程,表现为 FIFO,但这只是"实现上的巧合"。 - 工程红线 :严禁依赖该顺序 ,因为
notify唤醒后线程会进入EntryList重新竞争锁,而synchronized是非公平锁,最终执行顺序无法保证。
-
Condition.signal()(AQS /ReentrantLock配套)- 严格按 FIFO 顺序唤醒,唤醒的是等待队列头部线程。
- 不是随机,而且是设计约定,业务可以合理依赖。
一、synchronized + wait() / notify():WaitSet 里的秘密
synchronized 的重量级锁底层依赖 ObjectMonitor,它内部有两个关键队列:
- EntryList:等待获取锁的线程队列(竞争锁)。
- WaitSet :调用了
wait()后释放锁并挂起的线程队列(单向链表)。
notify() 到底怎么选线程?
从 JDK8 HotSpot 源码 看,notify() 会从 WaitSet 中取出链表头部的第一个等待线程 进行唤醒,因此肉眼观测到的现象是先入先出(FIFO)。
但是! 这恰恰就是 90% 的人掉坑的地方。
误区 ①:既然 HotSpot 是 FIFO,业务代码就能依赖这个顺序?
绝对不能。 原因有二:
-
这是 HotSpot 内部实现细节,不是规范。
Object.notify()的 Javadoc 写得很清楚:The choice is arbitrary and occurs at the discretion of the implementation.
任意 JVM 都有权实现成随机、优先级优先,明天 HotSpot 改成随机也完全合法。
-
唤醒顺序 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 是随机唤醒吗?",你就知道他其实想听的是 规范与实现的区别 ,以及锁竞争对执行顺序的最终影响。把这篇文章的逻辑理清,这个知识点你就彻底通关了。