第22题:wait、park 和 sleep 的区别?
📚 回答:
- 核心考点 :
wait()、park()和sleep()是 Java 中三种让线程暂停执行的机制,但大厂面试不会只问"方法归属、唤醒方式、锁特性",而是深入考察 底层实现原理 (ObjectMonitor vs Unsafe.park vs 系统调用)、许可机制(Permit) 、先 unpark 后 park 的语义保证 、中断响应的差异 ,以及 AQS 为什么用 LockSupport 而不是 wait/notify。面试官真正想判断的是:你是否能从 JVM 源码层面理解三种机制的本质差异,并能在工程实践中做出正确选型。
1. 七大维度全面对比
| 对比维度 | wait() / wait(long) |
LockSupport.park() |
Thread.sleep(long) |
面试踩坑点 |
|---|---|---|---|---|
| 方法归属 | Object 实例方法 |
LockSupport 静态方法 |
Thread 静态方法 |
park 不是 Thread 的方法! |
| 调用前提 | 必须持有对象锁 | 无限制 | 无限制 | 无锁调用 wait 抛异常 |
| 锁行为 | 释放锁 | 不释放锁 | 不释放锁 | park/sleep 在同步块内会阻塞其他线程 |
| 唤醒方式 | notify() / notifyAll() / 中断 / 超时 |
unpark() / 中断 |
超时自动恢复 / 中断 | park 不支持 notify! |
| 许可机制 | 无 | 有(二元许可,最多累积1个) | 无 | 先 unpark 后 park 不会阻塞 |
| 线程状态 | WAITING / TIMED_WAITING |
WAITING / TIMED_WAITING |
TIMED_WAITING |
park 也是 WAITING |
| 底层实现 | ObjectMonitor + 条件队列 | Unsafe.park(操作系统线程调度) | os::sleep(系统定时器) | 三者底层完全不同 |
| 使用场景 | 线程协作(生产者-消费者) | AQS、自定义同步器、无锁编程 | 延时执行、定时轮询 | park 不直接用于业务 |
2. 底层实现原理深度解析
- 2.1
wait()的底层------ObjectMonitor 条件队列
wait() 依赖 HotSpot 的 ObjectMonitor(C++ 实现),核心流程:
cpp
// ObjectMonitor::wait() 核心逻辑(简化)
void ObjectMonitor::wait(TRAPS) {
Thread* Self = THREAD;
// 1. 安全检查:必须持有锁
if (_owner != Self) {
THROW(vmSymbols::java_lang_IllegalMonitorStateException());
}
// 2. 创建 ObjectWaiter 节点
ObjectWaiter node(Self);
// 3. 释放锁(_owner = NULL)
exit(true, Self);
// 4. 加入 _WaitSet 条件队列
node._next = _WaitSet;
_WaitSet = &node;
// 5. 挂起线程(park)
park();
// 6. 被唤醒后:从 _WaitSet 移除,重新竞争锁
enter(Self);
}
关键特征 :wait() 释放锁是为了让其他线程能够获取锁并执行 notify(),否则将发生死锁。被唤醒后必须重新竞争锁才能从 wait() 返回。citation:0
- 2.2
park()的底层------Unsafe + 操作系统线程调度
LockSupport.park() 是 Java 并发包的基石,AQS、Semaphore、CountDownLatch 等全部基于它实现。底层调用 sun.misc.Unsafe.park():
cpp
// Unsafe.park() 底层实现(OpenJDK,简化)
UNSAFE_ENTRY(void, Unsafe_Park(JNIEnv *env, jobject unsafe,
jboolean isAbsolute, jlong time)) {
// 1. 检查是否已中断
if (Thread::is_interrupted(thread, false)) {
return; // 直接返回,不阻塞
}
// 2. 检查 permit:如果有许可,消费掉并直接返回
if (thread->osthread()->try_set_permit(false)) {
return;
}
// 3. 没有许可 → 挂起线程
thread->osthread()->set_state(MONITOR_WAIT);
// Linux: pthread_cond_wait
// Windows: WaitForSingleObject
os::park(thread, isAbsolute, time);
// 4. 被唤醒后:检查中断状态
if (Thread::is_interrupted(thread, true)) {
// 设置中断标志,但不抛异常
}
}
UNSAFE_END
核心特征:
- 无锁操作 :
park()不依赖任何对象锁,直接操作线程调度器。 - permit 机制 :每个线程有一个二元许可(0 或 1),
unpark()发放许可,park()消费许可。许可最多累积 1 个。 - 跨平台 :Linux 用
pthread_cond_wait,Windows 用WaitForSingleObject。citation:1
- 2.3
sleep()的底层------操作系统定时器
cpp
// JVM_Sleep 底层实现(简化)
JVM_ENTRY(void, JVM_Sleep(JNIEnv* env, jclass threadClass, jlong millis)) {
// 1. 检查中断状态
if (Thread::is_interrupted(THREAD, true)) {
THROW_MSG(vmSymbols::java_lang_InterruptedException(), ...);
}
// 2. 记录开始时间
jlong prev_time = javaTimeNanos();
// 3. 挂起线程(不操作 permit,不操作 Monitor)
thread->osthread()->set_state(MONITOR_WAIT);
os::sleep(thread, millis, false);
// 4. 检查中断(sleep 被中断会清除中断标志)
if (Thread::is_interrupted(THREAD, true)) {
THROW_MSG(vmSymbols::java_lang_InterruptedException(), ...);
}
}
sleep() 直接调用操作系统定时器,与锁、Monitor、permit 完全无关。citation:2
3. 许可机制(Permit)------park/unpark 的核心设计
- 3.1 什么是 Permit?
每个 Java 线程在操作系统层面关联一个 permit(许可),这是一个二元标志(0 或 1):
| 操作 | Permit 状态变化 | 行为 |
|---|---|---|
park() |
1 → 0 | 消费许可,直接返回 |
park() |
0 → 0 | 无许可,阻塞等待 |
unpark() |
0 → 1 | 发放许可,若线程正在 park 则唤醒 |
unpark() |
1 → 1 | 许可已存在,不累积(最多 1 个) |
- 3.2 "先 unpark 后 park" 的语义保证
这是 park/unpark 相比 wait/notify 最核心的优势:
java
// ✅ 正确:先 unpark 再 park,不会阻塞
LockSupport.unpark(threadA); // 发放 permit(此时 threadA 还没 park)
// ... 其他代码 ...
LockSupport.park(); // 消费 permit,直接返回,不会阻塞!
// ❌ 错误:先 notify 再 wait,信号丢失
// 如果 notify 时线程还没 wait,信号完全丢失,线程永远阻塞
obj.notify(); // 此时无人等待,信号浪费
// ... 其他代码 ...
obj.wait(); // 永远等不到上面的 notify!
关键差异 :unpark() 的 permit 可以预先发放并缓存(最多 1 个),而 notify() 没有缓存机制,发送时无人等待则信号丢失。citation:3
- 3.3 中断对 park 的影响
java
Thread thread = new Thread(() -> {
System.out.println("开始 park");
LockSupport.park(); // 被中断后:直接返回,不抛异常!
System.out.println("park 返回,中断状态:" + Thread.interrupted()); // true
});
thread.start();
Thread.sleep(100);
thread.interrupt(); // 中断 park 的线程
重要 :park() 被中断后 不抛出 InterruptedException ,只是让 park() 立即返回。中断标志位 不清除 (与 wait() 相同,与 sleep() 不同)。
4. 三种机制的中断响应差异
| 特性 | wait() |
park() |
sleep() |
|---|---|---|---|
| 中断时抛异常 | ✅ InterruptedException |
❌ 不抛异常 | ✅ InterruptedException |
| 中断后行为 | 从 wait 返回,需重新竞争锁 | 从 park 返回,继续执行 | 从 sleep 返回,继续执行 |
| 中断标志位 | 不清除 | 不清除 | 清除(置 false) |
| 代码处理 | catch + interrupt() 恢复 |
检查 Thread.interrupted() |
catch 即可 |
java
// wait() 中断处理
try {
synchronized (lock) { lock.wait(); }
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 恢复标志位
}
// park() 中断处理(无异常!)
LockSupport.park();
if (Thread.interrupted()) { // 检查中断状态
// 处理中断逻辑
}
// sleep() 中断处理
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 中断标志已被清除,如需保留需手动设置
Thread.currentThread().interrupt();
}
5. 为什么 AQS 用 LockSupport 而不是 wait/notify?
- 5.1 AQS 的设计需求
AbstractQueuedSynchronizer(AQS)是 ReentrantLock、Semaphore、CountDownLatch 的底层框架,它需要:
- 精确唤醒指定线程 :
unpark(thread)可以唤醒特定线程,而notify()随机唤醒。 - 无需持有锁:AQS 的等待队列管理不需要与某个对象的 Monitor 绑定。
- 先唤醒后阻塞的语义 :
unpark的 permit 机制避免了信号丢失。 - 更丰富的阻塞/唤醒控制:支持超时、截止时间、不响应中断等。
- 5.2 AQS 的 park/unpark 使用模式
java
// AQS.acquireQueued() 核心逻辑(简化)
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null;
failed = false;
return interrupted;
}
// ★ 阻塞前检查是否需要 park
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) // ★ 调用 LockSupport.park()
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
// AQS.release() 核心逻辑(简化)
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // ★ 调用 LockSupport.unpark()
return true;
}
return false;
}
AQS 使用 CLH 变体队列 管理等待线程,通过 LockSupport.park() 阻塞、LockSupport.unpark() 唤醒,完全脱离了 Object 的 Monitor 机制。citation:4
6. 生产环境避坑指南
- 6.1
park()不会释放锁
java
// ❌ 错误:在同步块内 park,导致死锁
synchronized (lock) {
LockSupport.park(); // 持有锁阻塞,其他线程无法获取锁
}
// ✅ 正确:park 前释放锁
synchronized (lock) {
// ... 业务逻辑
}
LockSupport.park(); // 无锁阻塞
- 6.2
park()被虚假唤醒后需重新检查条件
java
// ❌ 错误:park 后直接执行业务
LockSupport.park();
doWork(); // 可能被虚假唤醒,条件不满足!
// ✅ 正确:while 循环检查条件
while (!conditionMet) {
LockSupport.park();
}
doWork();
虽然 park() 的虚假唤醒概率比 wait() 低得多,但防御性编程原则仍建议用 while 循环。
- 6.3
unpark()可以重复调用,但 permit 不累积
java
LockSupport.unpark(threadA); // permit = 1
LockSupport.unpark(threadA); // permit = 1(不累积!)
LockSupport.unpark(threadA); // permit = 1(不累积!)
threadA.park(); // 消费 permit = 1,直接返回
threadA.park(); // permit = 0,永久阻塞!
注意 :多次 unpark() 不会累积多个 permit,最大为 1。
- 6.4
parkNanos()的精度问题
java
// parkNanos 依赖操作系统调度,实际等待时间 ≥ 指定时间
LockSupport.parkNanos(1_000_000L); // 约 1ms,实际可能 1~2ms
如果需要高精度定时,使用 java.time 或 ScheduledExecutorService。
- 6.5 不要在业务代码中直接使用
park/unpark
java
// ❌ 错误:业务代码直接操作 park
LockSupport.park(); // 难以维护,逻辑晦涩
// ✅ 正确:使用高层并发工具
semaphore.acquire(); // 语义清晰
condition.await(); // 与锁绑定,安全
CountDownLatch.await(); // 明确等待事件
park/unpark 是并发框架的底层工具,业务代码应使用 ReentrantLock、Semaphore、CountDownLatch 等高层抽象。
- 6.6
Thread.interrupted()会清除中断标志
java
LockSupport.park();
if (Thread.interrupted()) { // ★ 调用后中断标志被清除!
// 处理中断
}
// 后续代码无法感知中断了
如果需要保留中断状态,使用 Thread.isInterrupted()(不清除标志)。
7. 面试官追问与高分回答模板
- 追问 1:"
wait()、park()和sleep()有什么区别?"
低分回答 :"wait() 释放锁,park() 和 sleep() 不释放锁;wait() 用 notify 唤醒,park() 用 unpark 唤醒,sleep() 时间到自动醒。"(太浅,没有触及底层)
高分回答:
"三者的区别要从 底层实现、锁行为、许可机制、中断响应 四个维度分析:
- 底层实现 :
wait()基于 HotSpot ObjectMonitor 的_WaitSet条件队列;park()基于Unsafe.park()调用操作系统线程调度(Linuxpthread_cond_wait);sleep()基于操作系统定时器os::sleep。- 锁行为 :
wait()必须持有锁且会释放锁;park()和sleep()与锁完全无关,不释放锁。- 许可机制 :
park/unpark有独特的 permit 机制 (二元许可,最多累积 1 个),允许先unpark后park而不阻塞;wait/notify没有缓存机制,先notify后wait信号丢失。- 中断响应 :
wait()和sleep()被中断时抛InterruptedException,但wait()不清除中断标志,sleep()清除;park()被中断时不抛异常,只是让park()返回,中断标志不清除。核心记忆:
wait()= 协作(等别人通知,释放锁)park()= 调度(直接挂起线程,无锁)sleep()= 延时(自己睡一会,无锁)"
- 追问 2:"为什么 AQS 用
LockSupport.park()而不是Object.wait()?"
低分回答 :"因为 park() 更灵活。"(没有触及核心原因)
高分回答:
"AQS 选择
LockSupport.park()而非Object.wait()有四个核心原因:
- 精确唤醒 :
unpark(thread)可以唤醒指定线程;notify()随机唤醒一个,notifyAll()唤醒所有产生惊群效应。AQS 需要精确唤醒队列中的后继节点。- 无需持有锁 :
wait()必须在同步块内调用,AQS 的等待队列管理不需要与某个对象的 Monitor 绑定,使用park()更自由。- permit 防信号丢失 :
unpark()可以先于park()调用(permit 缓存),而notify()发送时无人等待则信号完全丢失。这在高并发竞争场景至关重要。- 功能更丰富 :
park()支持超时版本parkNanos()、parkUntil(),支持不响应中断;wait()功能单一。AQS 使用 CLH 变体队列管理等待线程,
park()阻塞、unpark()唤醒,完全脱离了 Object Monitor 机制,这是 Java 并发包高性能的底层基础。"
- 追问 3:"
park()的 permit 机制是什么?先 unpark 后 park 会怎样?"
高分回答:
"
park/unpark的 permit 是一个 二元标志(0 或 1),每个线程独立拥有一个:
unpark():将 permit 从 0 设为 1。如果线程正在park(),则唤醒它。park():检查 permit,若为 1 则消费掉(设为 0)并直接返回;若为 0 则阻塞等待。先 unpark 后 park:
javaLockSupport.unpark(threadA); // permit = 1 // ... 其他代码 ... LockSupport.park(); // 看到 permit = 1,消费掉并立即返回,不会阻塞!这是
park/unpark相比wait/notify的核心优势。notify()没有缓存机制,如果发送时无人 wait,信号永久丢失。注意 :permit 最多累积 1 个,多次
unpark()不会变成 2 或更多。"
- 追问 4:"
park()被中断时会抛异常吗?和wait()、sleep()有什么区别?"
高分回答:
"
park()被中断时 不抛出InterruptedException,这是三者最大的差异:
方法 中断时抛异常 中断标志位 wait()✅ 抛 InterruptedException不清除 sleep()✅ 抛 InterruptedException清除(置 false) park()❌ 不抛异常,只是让 park() 返回 不清除
park()的设计哲学是'静默处理':中断只是让阻塞的线程有机会检查状态并决定是否退出。AQS 正是利用这一特性,在parkAndCheckInterrupt()中检测中断并记录,但不立即响应,而是等到获取锁成功后再补发中断。代码示例:
javaLockSupport.park(); if (Thread.interrupted()) { // 检查中断状态(注意:interrupted() 会清除标志!) // 处理中断逻辑 } ```"
- 追问 5:"
park()和sleep()都不释放锁,它们有什么区别?"
高分回答:
"虽然两者都不释放锁,但底层机制和用途完全不同:
- 底层实现 :
park()基于操作系统线程调度(pthread_cond_wait/WaitForSingleObject),有 permit 机制;sleep()基于操作系统定时器(nanosleep/Sleep),无 permit 机制。- 唤醒方式 :
park()可被unpark()提前唤醒,也可被中断唤醒;sleep()只能等时间到或被中断,不能被外部'提前唤醒'(除了中断)。- 线程状态 :两者都进入
TIMED_WAITING(带超时版本)或WAITING(无超时版本),但 dump 信息不同:park()显示parking,sleep()显示sleeping。- 使用场景 :
park()用于并发框架(AQS)实现阻塞/唤醒;sleep()用于业务层的延时等待。关键区别:
park()可以被'主动唤醒'(unpark),sleep()只能'被动等待'(时间到)。"
- 追问 6:"如果让你实现一个自定义锁,你会用
wait/notify还是LockSupport.park/unpark?为什么?"
高分回答:
"我会选择
LockSupport.park/unpark,原因如下:
- 精确控制 :
unpark(thread)可以唤醒指定线程,而notify()随机唤醒,notifyAll()产生惊群效应。自定义锁需要精确唤醒队列中的下一个等待者。- 无锁依赖 :
park()不需要在同步块内调用,自定义锁可以自由设计队列结构(如 AQS 的 CLH 队列),不受 Object Monitor 的限制。- permit 防丢失 :高并发下,
unpark()可以先于park()执行,不会丢失信号;notify()没有这种保证。- 功能丰富 :支持超时
parkNanos()、截止时间parkUntil()、不响应中断等,满足复杂锁的需求。实际上,Java 官方的
ReentrantLock、ReadWriteLock、StampedLock全部基于 AQS 实现,而 AQS 的底层就是LockSupport.park/unpark。这是工业级验证的最佳实践。"
8. 方案选型速查表
| 业务场景 | 推荐方法 | 核心理由 |
|---|---|---|
| 生产者-消费者协作 | wait() / notify() |
释放锁,与 synchronized 配合 |
| 自定义同步器/锁 | LockSupport.park/unpark |
AQS 底层,精确唤醒,无锁依赖 |
| 延时执行、定时轮询 | Thread.sleep() |
简单直接,无需额外机制 |
| 需要超时控制的阻塞 | parkNanos() / wait(long) |
超时自动唤醒 |
| 需要精确唤醒指定线程 | LockSupport.unpark() |
唯一支持指定线程唤醒 |
| 不持有锁时的线程暂停 | sleep() / park() |
无需锁,不会抛异常 |
| 业务代码线程协作 | ReentrantLock / Semaphore |
语义清晰,避免直接操作 park |
💡 面试官想要的满分总结:
wait()、park()和sleep()的本质差异不是"释不释放锁",而是 设计目的和底层机制 的根本不同:
wait()是 对象协作 的工具,底层基于 ObjectMonitor 的_WaitSet条件队列,必须持有锁且会释放锁,被唤醒后需重新竞争锁。它解决的是"我等待某个条件,条件满足后别人通知我"的问题。
park()是 线程调度 的工具,底层基于Unsafe.park()调用操作系统线程调度器,与锁完全无关。它的核心创新是 permit 机制 ------二元许可允许先unpark后park而不阻塞,这是wait/notify无法做到的。AQS、Semaphore、CountDownLatch 全部基于park/unpark实现。
sleep()是 定时等待 的工具,底层基于操作系统定时器,与锁无关,时间到自动恢复。它解决的是"我自己暂停一段时间"的问题,不能被外部提前唤醒(除中断外)。生产环境中的选型原则:
- 线程协作 →
wait/notify(简单场景)或Condition.await/signal(复杂场景)- 自定义同步器 →
LockSupport.park/unpark(AQS 标准做法)- 延时执行 →
Thread.sleep()或ScheduledExecutorService- 业务代码 → 绝不直接使用
park/unpark,用高层并发工具面试中能讲清楚 permit 机制 、ObjectMonitor 源码 、AQS 为什么选 park,就已经超越了绝大多数候选人。
觉得对您有帮助,麻烦 点点关注啦 ,您的关注是我创作的最大动力~ 🎯