
作为一个常年跟并发编程打交道的 Java 开发我面试别人或者被面试的时候sleep()和wait()这对“卧龙凤雏”几乎是必考题。很多人背过八股文知道一个不释放锁、一个释放锁但真到生产环境排查问题或者写协作逻辑时经常把这两个方法混用踩出一堆线上事故。这篇内容我就从原理、源码、实战到面试追问把这俩方法的区别彻底聊透你看完不仅能应付面试更重要的是能在代码里做出正确选择。这两个方法表面看都是让线程“停下来”但背后的设计意图、运行机制和适用场景完全是两码事。Thread.sleep()是单纯的让线程暂停执行而Object.wait()是让线程主动释放锁并进入等待队列直到被其他线程唤醒。如果你刚学多线程这篇可以作为入门精读如果你已有几年经验也可以对照着查漏补缺看看自己在生产者消费者、连接池、分布式锁这些场景里有没有用错过。1. 看似都是“暂停”本质却截然不同先说个最常见的迷惑点很多初学者以为sleep()和wait()都是“让线程睡一会儿”甚至有人误以为wait()是Thread类的方法。这俩在“出身”上就完全不一样。1.1 方法的“户籍”决定了它们的分工Thread.sleep()是Thread类的静态方法通过Thread.sleep(1000)直接调用作用就是让当前线程暂停指定毫秒数。Object.wait()是Object类的实例方法必须通过某个对象调用比如lock.wait()作用是让当前线程在这个对象的监视器Monitor上等待。为什么wait()要设计在Object上而不是Thread上因为 Java 的每个对象天生就带一把锁和一个等待集合Wait Set。wait()的语义是“当前线程在某个对象的条件上等待”这个条件必须依附于某个对象实例。如果设计在Thread上就得为每个线程维护一套复杂的状态机反而不够灵活。这也是为什么wait()必须和synchronized配合使用的原因之一——你需要先持有这个对象的锁才能在这个对象上等待。再说调用前提。sleep()可以在任何地方随便调静态方法嘛不需要持有锁。但wait()必须在synchronized代码块或方法中调用否则直接抛IllegalMonitorStateException。这个设计不是故意为难你而是为了保证“检查条件、进入等待”这两个动作的原子性防止丢失唤醒信号。1.2 释放锁是两者最大的分水岭sleep()不释放锁wait()释放锁。这句话几乎所有面试者都会背但真正理解的人不多。举个例子你在一个synchronized方法里调用了Thread.sleep(3000)这 3000 毫秒内当前线程虽然不干活了但它手里那把对象的锁还紧紧攥着。其他线程想进入这个同步方法只能在门口等着干着急。换成wait()就完全不一样。线程调用obj.wait()的那一刻它会立刻释放持有obj的锁然后进入obj的等待集合。这时候其他线程就有机会获取同一把锁进入同步代码块执行。wait()释放锁是自动的、隐式的不需要你写额外的代码。我用一张表把这俩最关键的差异列出来方便对照对比项Thread.sleep(long millis)Object.wait() / Object.wait(long timeout)归属Thread 类的静态方法Object 类的实例方法调用前提无限制必须持有该对象的监视器锁否则抛 IllegalMonitorStateException释放锁不释放任何锁释放当前线程持有的该对象锁唤醒方式时间到了自动恢复需要 notify/notifyAll 唤醒或超时自动唤醒带超时参数时线程状态TIMED_WAITINGWAITING无超时/ TIMED_WAITING带超时异常抛 InterruptedException抛 InterruptedException设计意图定时暂停、模拟耗时、节流线程间协作、条件等待、生产者消费者模式这张表可以作为“面试背诵版”但你要想真正掌握还是得理解设计意图。sleep()是“你自己安排休息”wait()是“我等人来叫我等的时候我愿意把资源让给别人”。2. 核心区别逐个击破锁、线程状态与唤醒机制把基础概念理清楚后我们再往深挖一层。面试官最喜欢在这里加问几个为什么比如“为什么wait()必须配synchronized”“notify()和notifyAll()怎么选”“被唤醒后线程是立即执行吗”。2.1 为什么 wait() 必须在 synchronized 里面先说结论为了防止丢失唤醒Lost Wakeup问题。假设你写代码不用synchronized// 错误示范不能这么写 if (queue.isEmpty()) { wait(); // 这里会抛异常 } queue.add(data);这段代码有两处致命问题。第一wait()会直接抛IllegalMonitorStateException因为当前线程根本没持有queue对象的锁。第二就算你不调用wait()这个“检查-等待”的流程也不是原子的。想象一下两个线程并发执行线程 A 检查queue.isEmpty()发现队列为空还没执行wait()呢线程 B 抢先往队列里塞了一个元素并调用notify()。等 A 再执行wait()的时候B 的通知已经过去了A 永远睡下去没人叫醒。这就是经典的丢失唤醒。synchronized块保证了“检查条件、执行wait()”这两个步骤不被其他线程打断因为整段代码都在锁的保护之下。而notify()和wait()又必须在同一个锁对象上调用这就形成了完整的互斥与协作闭环。2.2 wake 之后的线程并不是立刻执行很多人误以为notify()一亮等待的线程就马上“活过来”继续跑。实际上不是的。notify()只是把等待集合里的一个线程捡出来让它从WAITING状态变为BLOCKED阻塞在锁上。被唤醒的线程要和其他线程一起重新竞争对象锁抢到了锁才能从wait()之后的代码继续执行。如果锁被其他线程占用它还得在锁外面排队等着。这里还有一个容易被忽略的细节被唤醒的线程不会从调用wait()的地方继续而是从wait()返回回到“等待之前”的边界。所以wait()的返回值无法区分“是被唤醒”还是“超时醒来”你只能通过重新检查条件来判断。正因为这样业内写等待逻辑有一条铁律wait()必须在循环里写而不是用if。比如synchronized (lock) { while (condition) { // 必须用 while 不是 if lock.wait(timeout); } // 这里执行的代码一定满足 condition 不成立 }为什么要这样由前面说的唤醒重抢锁特性衍生。假设你用if判断条件不满足就wait()唤醒后直接从if后面继续走。但此时条件可能还是不满足的——因为唤醒你的线程有可能没改变条件或者改变了条件但又被别的线程抢先消费了。这就是面试里常说的“虚假唤醒”Spurious Wakeup变体。用while重新检查可以保证线程被唤醒后重新确认条件不满足就继续循环等待满足才退出循环。2.3 线程状态的可观测差异sleep()和wait()在执行期间线程会进入不同的 JVM 状态排查线上问题用jstack抓线程栈时一眼就能分辨。Thread.sleep(1000)线程进入TIMED_WAITING状态jstack显示为java.lang.Thread.State: TIMED_WAITING (sleeping)。lock.wait()线程进入WAITING状态jstack显示为java.lang.Thread.State: WAITING (on object monitor)。lock.wait(1000)线程进入TIMED_WAITING状态jstack显示为java.lang.Thread.State: TIMED_WAITING (on object monitor)。jstack的(on object monitor)这个关键词很重要。它表示线程是在某个对象的监视器上等待等待理由是有另一个线程需要notify()来放行。如果你在看线程转储时发现很多WAITING (on object monitor)的线程基本可以判断这些线程是在做条件等待不是死锁也不一定是性能问题——除非它们等待的时间异常长。而TIMED_WAITING (sleeping)一般构不成“等待通知”的概念它就是单纯暂停。如果系统里大量线程都在sleeping那要考虑是不是有代码在循环里疯狂sleep()做无意义的重试这会拖累线程池吞吐量。2.4 sleep(0) 的隐藏用法还有个经常被忽视的小细节sleep(0)不是睡 0 毫秒直接跳过它会让当前线程让出 CPU触发一次系统线程调度器的重新调度。在高并发环境下sleep(0)偶尔可以用来打破线程饥饿的情况。不过这种方法比较“黑”实际项目里不建议为了调整优先级去依赖它容易引入不确定性。3. 实战经典生产者消费者与典型应用场景这里我不打算只贴概念咱们直接上码。先看wait()的正确打开姿势再看sleep()适合干的活。我把这两个场景串起来你就能理解它们是两个完全不同维度的工具。3.1 用 wait/notifyAll 实现生产者消费者public class ProducerConsumerDemo { private final LinkedListInteger queue new LinkedList(); private final int capacity 5; public synchronized void produce(int value) throws InterruptedException { // 必须用 while 循环唤醒后要重新检查容量 while (queue.size() capacity) { System.out.println(Thread.currentThread().getName() 队列满了生产者等待...); wait(); } queue.offer(value); System.out.println(Thread.currentThread().getName() 生产: value 当前队列大小: queue.size()); // 消费线程可能在等队列非空全部唤醒 notifyAll(); } public synchronized int consume() throws InterruptedException { while (queue.isEmpty()) { System.out.println(Thread.currentThread().getName() 队列空了消费者等待...); wait(); } int value queue.poll(); System.out.println(Thread.currentThread().getName() 消费: value 当前队列大小: queue.size()); notifyAll(); return value; } public static void main(String[] args) { ProducerConsumerDemo demo new ProducerConsumerDemo(); // 两个生产者 for (int i 0; i 2; i) { int producerId i; new Thread(() - { for (int j 0; j 10; j) { try { demo.produce(producerId * 100 j); Thread.sleep(100); // 模拟生产耗时 } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; } } }, Producer- i).start(); } // 三个消费者 for (int i 0; i 3; i) { new Thread(() - { while (true) { try { demo.consume(); Thread.sleep(200); // 模拟消费耗时 } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; } } }, Consumer- i).start(); } } }这里有几个细节值得提。第一wait()在produce和consume里用的都是while不是if。多生产者多消费者场景下如果只用if经常会出现“生产者被唤醒后发现队列还是满的”这种情况然后生产出非法数据或者死锁。你可以试试把while改成if跑一下大概率会出现数组越界或线程卡死。第二用的是notifyAll()而不是notify()。为什么因为notify()只会唤醒等待集合中的某一个线程如果唤醒的是同类线程比如仍然是生产者而当前需要的是消费者被唤醒那就会出现“互相等待、没人干活”的假死状态。notifyAll()把全部线程都唤醒虽然竞争激烈一点但至少不会因为“选错对象”而卡住。在仅有一个生产者和一个消费者的简单模型里notify()没太大问题但在多生产多消费模型里notifyAll()才是安全选择。第三生产者的notifyAll()放在queue.offer()之后是为了确保已经持有锁的情况下完成队列结构变更再通知消费者。如果notifyAll()写在offer()之前极端情况下有可能消费者被唤醒但队列里还没有数据醒过来发现是空队列又睡回去白白浪费一次唤醒。3.2 sleep 的正确场景定时任务、重试退避、限流sleep()最典型的用途是“不需要和别人协作单纯想把当前线程暂停一段时间”。比如第三方接口调用失败后的重试退避public class RetryDemo { private static final int MAX_RETRY 3; private static boolean callRemoteApi() { // 模拟远程调用一半概率失败 return Math.random() 0.5; } public static void main(String[] args) { boolean success false; for (int i 1; i MAX_RETRY; i) { success callRemoteApi(); if (success) { break; } System.out.println(第 i 次调用失败2 秒后重试); if (i MAX_RETRY) { try { Thread.sleep(2000); // 不涉及锁单纯等待 } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; } } } System.out.println(success ? 调用成功 : 重试次数已用完); } }这种场景里你要是用wait()反而不对因为你并没有一个“合作线程”来唤醒你也没持有锁你只是想单纯地等 2 秒。sleep()是最直接的工具。另一个常见用途是简单限流。比如一个批量任务每处理 100 条数据主动sleep(1000)把频率降下来避免打死下游数据库。注意这属于无奈之举真正的流量控制应该用RateLimiter、Semaphore这类工具sleep()只适合临时应急或者并发度很低的场景。3.3 一个让你线上故障的混用陷阱我见过不少线上事故都是用户把wait()当sleep()用或者反着来。这里分享一个高危场景在持有锁的情况下sleep()导致系统吞吐量瞬间归零。public class BadLockHoldingDemo { private static final Object LOCK new Object(); public static void main(String[] args) { Runnable task () - { synchronized (LOCK) { System.out.println(Thread.currentThread().getName() 拿到锁准备 sleep); try { Thread.sleep(5000); // 危险操作持有锁睡觉 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }; Thread t1 new Thread(task, t1); Thread t2 new Thread(task, t2); t1.start(); try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } t2.start(); System.out.println(t2 已经启动但要等 t1 释放锁预计 5 秒后才会执行); } }t2 虽然已经启动且线程状态是RUNNABLE但它实际上卡在锁外面一点活都干不了。如果这个LOCK是某个核心服务的锁持有它的线程在 sleep整个服务的并发能力会直接下降到你无法接受的水平。遇到这种情况正确的做法是重构代码把sleep()挪出synchronized块先释放锁再去睡觉。或者用wait(long timeout)替代让出锁的同时指定超时时间这样至少其他线程有机会拿到锁干活。4. 面试高频追问与生产避坑清单这部分既是面试突击也是实战复盘。我尽量把问题以“面试官追问”的形式列出来答案就是我要讲的内容。4.1 为什么 wait() 一定要配合 while 而不是 if这个问题前面已经解释过了这里再集中强调一遍因为面试问到频率极高。if只有一个入口判断线程从wait()苏醒后不会重新检查条件而wait()苏醒的时机不受你控制。此外JDK 官方文档明确提到线程可能在没有被notify()、没有被中断、没有超时的情况下被唤醒也就是“虚假唤醒”。这种情况在实践中虽然少见但它确实在底层实现里有发生的可能参考 LockSupport 的文档说明。所以while循环是双保险既防止并发修改导致的旧条件过期又天然兼容虚假唤醒。你把while换成if就等于放弃了 JVM 给的条件保障。4.2 notify() 和 notifyAll() 到底怎么选一句话能确认生产者消费者模型里“只需要唤醒一个特定角色”时可以用notify()任何不确定的情况下用notifyAll()。notify()选择等待集合里的哪一个线程是不确定的有可能选到一个不相关角色的线程。跟 3.1 例子一样多生产多消费场景下notify()可能唤醒生产者但真正该被唤醒的是消费者于是系统假死。notifyAll()会唤醒所有等待线程它们通过while条件再次竞争最终只有真正满足条件的线程能继续执行。补充一点notify()也不是一无是处。当你的线程池里所有线程都在同一个条件下等待比如“任务队列不为空”且你只需要让一个线程来处理新任务notify()可以避免“惊群效应”——一大群线程同时醒来互相抢锁最后只有一个拿到锁其他白醒一趟。JDK 里LinkedTransferQueue这类高性能队列的实现就用到了这种优化。只是这种优化非常微妙业务代码里一般轮不到你来考虑。4.3 sleep 和 wait 哪个更能体现“协作”答案显然是wait()。sleep()是独白它不需要任何其他线程参与时间到了自然醒。wait()是对话它依赖其他线程通过notify()来传递“条件已满足”的信号。这是两种编程模型的分界线sleep()适合时间驱动的逻辑而wait()适合事件驱动、协作式的逻辑。如果你在某个多线程协作流程里用sleep()代替wait()来“硬等”条件成立比如轮询某个标志位// 反面教材忙等待 while (!flag) { Thread.sleep(10); // 轮询等另一个线程置位 flag }这个写法虽然能跑但有两个问题。第一存在最多 10ms 的延迟flag 刚变为 true 你不会立刻感知。第二如果flag永远不会变 true这个线程就会永远睡下去你也不知道它卡在哪。而用wait()的方式另一个线程置位后主动notify()等待线程立刻被唤醒没有轮询延迟协作更精确。4.4 生产环境最容易踩的五个坑我把实际工程里容易踩的坑整理成清单每一条都有血泪教训在背后。不要在持有锁的代码块里调用sleep()。除非你能明确接受这段阻塞时间内其他线程完全无法访问共享资源。如果确实需要停一下先释放锁再 sleep或者直接改用wait(long)。不要拿 String 常量或者整型包装类做锁对象调wait()。String在 JVM 里可能被 intern多个不相关的地方共用同一个字符串字面量导致你 notify 的线程根本不在你 think 的对象上等着。Integer、Long这类包装类有缓存机制-128 到 127同样容易埋雷。请用private final Object lock new Object()这种专用锁对象。wait()和notify()必须成对出现在同一个锁对象上。如果生产者在A对象上 wait消费者在B对象上 notify两边永远对不上话。排查这种问题时jstack里看WAITING (on object monitor)后面的具体对象能帮你快速定位。唤醒后不是“万事大吉”必须检查自己的业务条件。notifyAll()唤醒了十个线程只有一个抢到锁剩下九个重新阻塞。但那唯一一个抢到锁的线程也要重新跑while条件确保它此刻确实可以干活。这叫“二次检查”是并发编程的基本功。碰到InterruptedException要么恢复中断状态要么明确向外层传递。错误的吞异常方式会让上层逻辑无法感知线程已被中断。正确做法是优先Thread.currentThread().interrupt()重新设置中断标记再交给上层决定怎么处理。4.5 如何在线上一眼定位 wait/sleep 引起的问题排查线上问题第一步永远是抓线程栈。常规操作是jps -l找到 Java 进程号然后jstack pid导出线程栈。在上面能找到java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) at com.example.ProducerConsumerDemo.produce(ProducerConsumerDemo.java:15)注意两个信息on object monitor表明是wait()后面的堆栈会直接给出代码行号你立刻能跳到源码找到那行wait()。同理TIMED_WAITING (sleeping)对应sleep()。如果线上出现大量线程都在WAITING (on object monitor)且业务长时间没有进展多半是某个notify()没被正确调用、或者while条件永远不成立。这时候优先查看有没有线程持有锁卡在sleep()或某个耗时长任务里。持有锁的线程往往显示RUNNABLE或者TIMED_WAITING (sleeping)它占着茅坑不拉屎其他线程只能在锁上排队进 Wait Set。5. 进阶从 wait/sleep 到更现代的并发工具理解了底层机制你就该知道业务代码里其实不需要天天亲自操作wait()和notify()。JDK 替你封装了更安全、更高效的实现。5.1 ReentrantLock Condition 替代 wait/notifyLock接口配合Condition提供了和Object.wait/notify等价的能力但更灵活。import java.util.LinkedList; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; public class ConditionDemo { private final ReentrantLock lock new ReentrantLock(); private final Condition notEmpty lock.newCondition(); private final Condition notFull lock.newCondition(); private final LinkedListInteger queue new LinkedList(); private final int capacity 5; public void produce(int value) throws InterruptedException { lock.lock(); try { while (queue.size() capacity) { notFull.await(); } queue.offer(value); System.out.println(生产: value 当前队列大小: queue.size()); notEmpty.signal(); } finally { lock.unlock(); } } public int consume() throws InterruptedException { lock.lock(); try { while (queue.isEmpty()) { notEmpty.await(); } int value queue.poll(); System.out.println(消费: value 当前队列大小: queue.size()); notFull.signal(); return value; } finally { lock.unlock(); } } }对比原生wait/notifyCondition最大的改进是支持多个等待队列。这里生产者等待的是notFull队列队列满了就等消费者等待的是notEmpty队列队列空了就等。signal()只唤醒当前 Condition 队列中的线程不会像notify()那样可能唤醒“不相关角色”从根上杜绝了假死问题。5.2 高级并发容器更省心实操中我大多数情况下根本不会亲手写生产者消费者直接上ArrayBlockingQueue、LinkedBlockingQueue这类并发容器内部封装好了锁和条件队列。生产者和消费者各自调用put()和take()阻塞和唤醒由容器内部完成业务代码完全不用关心wait()和notify()。import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; public class BlockingQueueDemo { public static void main(String[] args) { BlockingQueueInteger queue new ArrayBlockingQueue(5); Thread producer new Thread(() - { for (int i 0; i 20; i) { try { queue.put(i); // 队列满会自动等待 System.out.println(生产: i); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; } } }); Thread consumer new Thread(() - { while (true) { try { int value queue.take(); // 队列空会自动等待 System.out.println(消费: value); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; } } }); producer.start(); consumer.start(); } }同样的功能用高级容器代码量减少了一大半而且由 JDK 作者级专家优化过的并发算法远比你自己造的轮子可靠。5.3 CompletableFuture 和响应式思维再往后走一步。多线程协作的本质是“任务依赖某个异步结果”CompletableFuture提供了更优雅的编排方式CompletableFuture.supplyAsync(() - { // 模拟耗时任务 return hello; }).thenApply(String::toUpperCase) .thenAccept(System.out::println);在这个模型里你完全不需要关系线程是sleep()还是wait()因为每个阶段之间的流转用的是回调注册机制底层由ForkJoinPool管理线程的调度。站在面试角度来看如果你能从这个演进脉络讲明白wait/sleep是最底层的工具而现代框架已经帮你封装了线程协作的细节那你的深度就出来了。结尾个人使用后的几点体会与建议我最早学这两个方法时也是背“sleep 不释放锁wait 释放锁”但真正开窍是在一个故障排查的深夜。当时线上有个订单超卖问题我看线程栈发现大量消费者卡在WAITING (on object monitor)上游生产者却因为持锁sleep()迟迟不释放锁整个系统吞吐量掉到 0。那次以后我才真正意识到sleep()和wait()不只是面试题它们直接决定了你的系统是“自我阻塞”还是“协作畅行”。我的经验是在写并发代码前先问自己一个问题——我需要的是“定时暂停”还是“条件等待”如果是前者优先考虑Thread.sleep()如果是后者优先考虑wait/notify或者更高级的Condition、并发容器。能用BlockingQueue就不要手写wait()能用Condition就不要用Object.wait能用CompletableFuture就不要自己管线程协作。最后分享一个小习惯每写一段涉及wait()的代码我都会检查三件事。第一wait()是否在while循环里。第二notify()或notifyAll()是否在条件真正变化之后调用。第三锁对象的生命周期是不是独立且可控的绝对不能拿共享的字符串当锁。这三条检查下来能避开绝大多数的线程协作问题。