Java 中 sleep 和 wait 方法的区别详解
在多线程编程中,sleep() 和 wait() 是两个基础但极易混淆的方法。它们都用于控制线程的执行节奏,但在所属类、锁机制、唤醒方式、使用场景等方面有着本质区别。本文将从底层原理到实际代码,全面剖析二者的差异。
一、基本概念
1. sleep() —— Thread 的静态方法
sleep() 是 java.lang.Thread 类的 静态本地方法,其作用是让 当前正在执行的线程 暂停指定的毫秒数(可选的纳秒数),进入 超时等待(Timed Waiting) 状态。
public static native void sleep(long millis) throws InterruptedException;
2. wait() —— Object 的实例方法
wait() 是 java.lang.Object 类的 实例方法,其作用是将 持有该对象监视器锁的线程 释放锁并进入 等待集(Wait Set),直到其他线程在该对象上调用 notify() / notifyAll() 或者等待时间到期。
public final void wait() throws InterruptedException;
public final void wait(long timeout) throws InterruptedException;
二、核心区别对比表
| 对比维度 | sleep() |
wait() |
|---|---|---|
| 所属类 | java.lang.Thread(静态方法) |
java.lang.Object(实例方法) |
| 释放锁 | ❌ 不释放 任何锁 | ✅ 释放 持有的对象监视器锁 |
| 调用前提 | 任何地方都可以直接调用 | 必须在 synchronized 同步块/方法中调用 |
| 唤醒方式 | 时间到期后自动苏醒 | notify() / notifyAll() 或超时唤醒 |
| 线程状态 | TIMED_WAITING |
WAITING 或 TIMED_WAITING |
| 用途 | 暂停执行、模拟延迟、控制频率 | 线程间协作、生产者-消费者模式 |
| 是否清除中断状态 | 抛出 InterruptedException 后清除 |
抛出 InterruptedException 后清除 |
| 静态/实例 | 静态方法(Thread.sleep()) |
实例方法(obj.wait()) |
| 语法要求 | 无特殊限制 | 必须在 synchronized 上下文中 |
三、关键区别详解
3.1 释放锁 —— 这是最本质的区别
sleep() 不释放锁。 即使线程在 synchronized 块中调用 sleep(),它依然 持有所有已获得的锁,其他线程无法进入该同步块。
public class SleepDoesNotReleaseLock {
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
synchronized (lock) {
System.out.println("线程1 进入同步块,开始 sleep(3000)...");
try {
Thread.sleep(3000); // 持有锁不释放
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("线程1 sleep 结束,退出同步块");
}
});
Thread t2 = new Thread(() -> {
System.out.println("线程2 尝试获取锁...");
synchronized (lock) {
System.out.println("线程2 成功获取锁!");
}
});
t1.start();
Thread.sleep(100); // 确保 t1 先拿到锁
t2.start();
}
}
输出结果:
线程1 进入同步块,开始 sleep(3000)...
线程2 尝试获取锁...
(约 3 秒后...)
线程1 sleep 结束,退出同步块
线程2 成功获取锁!
可以看到,t2 必须等到 t1 sleep 完毕后释放锁,才能进入同步块。
wait() 释放锁。 调用 wait() 后,当前线程会 立即释放 该对象上的监视器锁,并进入等待集,给其他线程提供了获取锁的机会。
public class WaitReleasesLock {
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
synchronized (lock) {
System.out.println("线程1 进入同步块,调用 wait() 释放锁...");
try {
lock.wait(); // 释放锁 + 进入等待集
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("线程1 被唤醒,继续执行");
}
});
Thread t2 = new Thread(() -> {
synchronized (lock) {
System.out.println("线程2 获取到了锁,调用 notify()...");
lock.notify(); // 唤醒等待集中的线程
System.out.println("线程2 执行完成");
}
});
t1.start();
Thread.sleep(100); // 确保 t1 先拿到锁并 wait()
t2.start();
}
}
输出结果:
线程1 进入同步块,调用 wait() 释放锁...
线程2 获取到了锁,调用 notify()...
线程2 执行完成
线程1 被唤醒,继续执行
t1 调用
wait()后释放锁,t2 立即进入同步块;t2 调用notify()后 t1 被唤醒。
3.2 唤醒条件
| 方法 | 唤醒方式 | 说明 |
|---|---|---|
sleep(millis) |
超时自动唤醒 | 等待指定毫秒数后自动恢复运行 |
wait() |
需要 notify / notifyAll 唤醒 |
无限期等待,必须有其他线程显式唤醒 |
wait(timeout) |
超时唤醒 或 notify 唤醒 | 两者中先发生的将唤醒线程 |
sleep() 只会因为时间到期或被中断而苏醒,不需要外部干涉。
wait() 有四个唤醒条件:
- 其他线程调用该对象的
notify()方法 - 其他线程调用该对象的
notifyAll()方法 - 等待超时(使用
wait(timeout)时) - 线程被中断(抛出
InterruptedException)
3.3 使用前提(synchronized 要求)
wait() 必须 在同步块或同步方法中调用,否则会抛出 IllegalMonitorStateException:
// ❌ 错误用法
Object obj = new Object();
obj.wait(); // IllegalMonitorStateException!
// ✅ 正确用法
synchronized (obj) {
obj.wait(); // 合法
}
sleep() 没有此限制,可以在任何地方调用:
// ✅ 任何时候都可以
Thread.sleep(1000);
四、深度剖析:底层原理
4.1 Java 对象的内存布局与 Monitor
每个 Java 对象在内存中关联一个 Monitor(监视器锁)。synchronized 区域背后的本质是线程获取对象的 Monitor 所有权。
- _EntryList(入口集):等待获取锁的线程集合。
- _WaitSet(等待集):调用
wait()后释放锁并等待被唤醒的线程集合。
流程图:
┌─────────────────────┐
│ Owner (持有锁线程) │
└──────────┬──────────┘
│
┌─────────────┼─────────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ EntryList │ │ WaitSet │ │ 终止 │
│ (等待锁) │ │ (等待唤醒) │ │ │
└──────────┘ └──────────┘ └──────────┘
4.2 调用 wait() 时的流程
- 当前线程必须持有该对象的 Monitor(否则抛异常)。
- 将当前线程放入该对象的 WaitSet 中。
- 释放 该对象的 Monitor(锁)。
- 线程状态变为
WAITING或TIMED_WAITING。 - 被唤醒后(
notify/超时),线程从 WaitSet 移除,重新竞争 Monitor,获取到锁后才继续执行。
4.3 调用 sleep() 时的流程
- 当前线程暂停执行指定时间。
- 不释放任何锁(即使处于
synchronized块中)。 - 线程状态变为
TIMED_WAITING。 - 时间到期后恢复运行。
五、实战场景对比
5.1 使用 sleep() —— 轮询/定时/延迟
public class SleepDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println("任务开始...");
// 每 2 秒检查一次状态(模拟轮询)
for (int i = 0; i < 3; i++) {
Thread.sleep(2000);
System.out.println("轮询第 " + (i + 1) + " 次...");
}
System.out.println("任务结束");
}
}
应用场景: 定时任务、控制循环频率、模拟网络延迟、动画帧间隔。
5.2 使用 wait() / notify() —— 生产者-消费者
import java.util.LinkedList;
import java.util.Queue;
public class ProducerConsumer {
private static final Queue<Integer> queue = new LinkedList<>();
private static final int CAPACITY = 5;
public static void main(String[] args) {
Thread producer = new Thread(() -> {
int value = 0;
while (true) {
synchronized (queue) {
while (queue.size() == CAPACITY) {
try {
System.out.println("队列已满,生产者等待...");
queue.wait(); // 释放锁等待
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
queue.offer(value);
System.out.println("生产者生产: " + value);
value++;
queue.notifyAll(); // 唤醒消费者
}
// 模拟生产间隔
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
Thread consumer = new Thread(() -> {
while (true) {
synchronized (queue) {
while (queue.isEmpty()) {
try {
System.out.println("队列已空,消费者等待...");
queue.wait(); // 释放锁等待
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
int value = queue.poll();
System.out.println("消费者消费: " + value);
queue.notifyAll(); // 唤醒生产者
}
// 模拟消费间隔
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
producer.start();
consumer.start();
}
}
输出片段示例:
生产者生产: 0
生产者生产: 1
生产者生产: 2
生产者生产: 3
生产者生产: 4
队列已满,生产者等待...
消费者消费: 0
消费者消费: 1
...
这里
wait()/notifyAll()是 线程间协作 的核心机制,而Thread.sleep()仅用于模拟生产和消费的耗时。
5.3 混合使用场景:带超时的等待
wait(timeout) 可以做到类似 sleep() 的效果,但 同时释放了锁:
synchronized (lock) {
// 等待最多 3 秒,期间其他线程可获取锁
lock.wait(3000);
// 3 秒后自动苏醒,或被 notify 提前唤醒
}
而 sleep() 虽然也能暂停,但 会一直霸占锁,可能导致系统性能下降或死锁风险。
六、重要注意事项
6.1 虚假唤醒(Spurious Wakeup)
wait() 可能在没有被 notify()、未超时、也未中断的情况下被唤醒 —— 这就是 虚假唤醒。JVM 规范允许这种情况发生。
解决方案: 始终在循环中检查等待条件,而不是使用 if。
// ❌ 错误:可能因虚假唤醒导致条件不满足而继续执行
synchronized (lock) {
if (!condition) {
lock.wait();
}
// 条件可能不满足就走到这里
}
// ✅ 正确:循环检查,确保条件满足后才继续
synchronized (lock) {
while (!condition) {
lock.wait();
}
// 条件一定满足
}
Java 官方文档和 Object.wait() 的 Javadoc 都明确推荐使用 while 模式。
6.2 中断处理
两者都会抛出 InterruptedException,正确的处理方式是 恢复中断状态(传递中断信号),而不是直接吞掉异常:
// ❌ 错误:忽略中断
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 什么都不做 —— 中断信号丢失!
}
// ✅ 正确:恢复中断状态
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 重新设置中断标记
// 根据业务逻辑决定是否退出
}
6.3 notify() vs notifyAll()
| 方法 | 行为 | 风险 |
|---|---|---|
notify() |
随机唤醒 WaitSet 中的一个线程 | 如果唤醒的线程不满足条件,可能造成死锁 |
notifyAll() |
唤醒 WaitSet 中所有线程 | 更安全,但可能引起不必要的竞争 |
建议: 除非你能确保只有一个线程在等待且条件唯一,否则优先使用 notifyAll()。
七、常见面试问题
Q1: 为什么 wait() 在 Object 类中,而 sleep() 在 Thread 类中?
wait()操作的是 对象监视器(Monitor),每个对象都有一个 Monitor,所以放在 Object 中合情合理。sleep()操作的是 当前线程 的执行状态,属于线程级别的行为,因此放在 Thread 中。
Q2: 调用 wait() 后,线程会释放哪些锁?
只会释放调用 wait() 的那个对象的 Monitor 锁。如果线程持有多把锁(嵌套 synchronized),其他对象的锁仍然持有,不会释放。
synchronized (lockA) {
synchronized (lockB) {
lockA.wait(); // 只释放 lockA 的锁,lockB 仍然持有
}
}
Q3: sleep(0) 有什么作用?
Thread.sleep(0) 会触发操作系统进行一次 线程调度,让当前线程重新竞争 CPU 时间片。常用于某些需要让步(yield)但又要保留锁的场景。
Q4: wait() 和 sleep() 哪个效率更高?
没有绝对的"效率更高"之说,二者用途不同:
- 需要锁协作 → 用
wait()/notify() - 单纯暂停不涉及锁 → 用
sleep() - 用
sleep()替代wait()来做线程协作是 反模式,会导致性能问题和逻辑错误。
八、总结
| 核心要点 | sleep() |
wait() |
|---|---|---|
| 本质 | 让线程暂停执行一段时间 | 线程间通信、协调工作 |
| 锁行为 | 不释放锁 | 释放对象锁 |
| 调用约束 | 无限制 | 必须在 synchronized 中 |
| 唤醒 | 自动唤醒 | notify() / notifyAll() |
| 常见用途 | 定时、延迟、控制频率 | 生产者-消费者、条件等待 |
| 线程状态 | TIMED_WAITING |
WAITING / TIMED_WAITING |
一句话总结:
sleep()是"我累了,休息一会儿继续干活";wait()是"我缺东西,等别人送来再干"。
sleep()让你暂停,但不放手(锁)—— 适用于计时、轮询、限流。wait()让你放手(锁)等待条件满足 —— 适用于线程间协作、资源调度。
掌握它们的区别,是编写正确、高效、无死锁的多线程程序的基础。
希望本文对你理解 Java 并发编程有所帮助。欢迎留言讨论!