几周前在帮团队做 Code Review 时,看到一个线程池任务里用 Thread.sleep(3000) 代替等待条件成立的写法——任务里明明在轮询一个共享标记位,却让线程白白睡死三秒,不仅响应延迟拉满,CPU 也被空转的检查逻辑折腾得够呛。我当时就问了一句:“这里怎么不用 wait/notify?”得到的回答是“怕 wait 把锁释放了出问题”。这个回答让我意识到,很多写过几年 Java 的老手,对 Thread.sleep() 和 Object.wait() 的理解也停留在“一个会释放锁、一个不会”的表面,真到了并发协作的复杂场景里,要么不敢用 wait,要么用 sleep 硬顶。这篇文章就把这对经典组合彻底掰开揉碎,从锁、状态、唤醒、中断这些底层机制讲到实际工程里的选型,看完你不仅能应付面试,更重要的是能在生产代码里做出正确的选择。
1. 核心区别一:方法归属与调用前提
1.1 所属类与静态/实例方法的本质差异
Thread.sleep() 是 java.lang.Thread 类的静态方法,调用时直接 Thread.sleep(1000) 就行,它操作的是“当前正在执行的线程”。这是一个很关键的语义:你写 t1.sleep(1000),实际睡着的也不是 t1,而是当前线程。静态方法本质上和具体线程实例无关。
Object.wait() 是 java.lang.Object 的实例方法,调用时必须是某个具体的对象,比如 lock.wait()。它操作的是“当前线程”和“这个对象的内置锁(monitor)”之间的关系。因为所有类都继承自 Object,所以理论上任何对象都能当锁、都能调 wait。
一个是静态工具方法,一个是需要依托对象的实例方法——这决定了它们的使用场景分工:sleep 就是单纯让线程暂停,不掺和任何协作逻辑;wait 天生就是为“线程间通过某个共享对象进行协调”设计的。
1.2 调用前置条件:wait 的强制性要求
Object.wait() 有一个硬性要求:调用线程必须持有该对象的监视器锁(monitor lock)。也就是说,wait() 必须在 synchronized 修饰的代码块或方法内部调用,否则运行时直接抛 java.lang.IllegalMonitorStateException。
java复制// 错误示范:没有持有锁就调用 wait
Object lock = new Object();
public void bad() {
try {
lock.wait(); // 抛 IllegalMonitorStateException
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
为什么 JVM 要强制这个约束?因为 wait() 的语义是“让出 CPU 并释放锁,进入等待队列”,如果线程根本没持有锁,它拿什么释放?这个设计不是刁难开发者,而是从机制上杜绝“无锁瞎等”的混乱状态。而 Thread.sleep() 没有任何前置条件,任何地方都能调用,它根本不关心锁的状态。
注意:
wait()还有几个重载版本——wait()、wait(long timeout)、wait(long timeout, int nanos)。wait(0)代表无限期等待,直到被唤醒。这一点和sleep(0)的意义完全不同,后面详细说。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心区别二:锁的持有与释放行为
2.1 sleep 不释放锁:一个最容易踩坑的行为
Thread.sleep() 在执行期间不会释放任何已经持有的锁。下面这段代码用生活化的场景比喻:你把唯一一把洗手间钥匙(锁)带进去了,然后在里面睡了三小时,门外所有人都得等着。
java复制public class SleepHoldsLock {
private static final Object LOCK = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (LOCK) {
System.out.println("t1 拿到锁,开始 sleep 5秒");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("t1 sleep 结束,释放锁");
}
});
Thread t2 = new Thread(() -> {
synchronized (LOCK) {
System.out.println("t2 拿到锁");
}
});
t1.start();
// 确保 t1 先执行并拿到锁
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
t2.start();
}
}
运行这段代码,t2 的 "t2 拿到锁" 必须等 t1 睡满 5 秒后才会打印。这说明 sleep 期间锁一直被占着。这点在生产环境非常危险:如果你在持锁状态下调用 sleep,其他等待锁的线程全部阻塞,轻则响应变慢,重则连锁超时、线程池队列堆满。持锁休眠几乎等同于制造一次小范围死锁。
2.2 wait 释放锁:线程协作的核心机制
Object.wait() 执行后,当前线程会立即释放该对象的监视器锁,然后进入该对象的等待集合(wait set)。这是 wait/notify 协作机
