菜单

Administrator
发布于 2026-05-18 / 1 阅读
0
0

Java 中 sleep 和 wait 方法的区别

Java 中 sleepwait 方法的区别详解

在多线程编程中,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 WAITINGTIMED_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() 有四个唤醒条件:

  1. 其他线程调用该对象的 notify() 方法
  2. 其他线程调用该对象的 notifyAll() 方法
  3. 等待超时(使用 wait(timeout) 时)
  4. 线程被中断(抛出 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() 时的流程

  1. 当前线程必须持有该对象的 Monitor(否则抛异常)。
  2. 将当前线程放入该对象的 WaitSet 中。
  3. 释放 该对象的 Monitor(锁)。
  4. 线程状态变为 WAITINGTIMED_WAITING
  5. 被唤醒后(notify/超时),线程从 WaitSet 移除,重新竞争 Monitor,获取到锁后才继续执行。

4.3 调用 sleep() 时的流程

  1. 当前线程暂停执行指定时间。
  2. 不释放任何锁(即使处于 synchronized 块中)。
  3. 线程状态变为 TIMED_WAITING
  4. 时间到期后恢复运行。

五、实战场景对比

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 并发编程有所帮助。欢迎留言讨论!


评论