深入理解 sleep() 与 wait():从基础到监视器队列
前言看似都是“让线程停下来”背后的原理却完全不同在 Java 并发编程中sleep()和wait()是两个经常被拿来比较的方法。很多初学者甚至有一定经验的开发者也容易混淆它们。今天这篇文章我们就从基础区别一路深入到监视器锁的队列机制彻底搞懂这两个方法。一、五分钟快速掌握核心区别先上结论后面再展开细节。对比维度sleep()wait()所属类Thread类的静态方法Object类的实例方法调用前提任意地方均可调用必须在同步块/同步方法中是否释放锁❌ 不会释放任何锁✅ 释放当前对象的锁唤醒方式时间到自动唤醒需要notify/notifyAll唤醒也可超时唤醒典型场景暂停执行、模拟耗时线程间通信、等待条件满足二、从代码看差异2.1 sleep() 用法publicclassSleepDemo{publicstaticvoidmain(String[]args){System.out.println(开始睡眠...);try{Thread.sleep(3000);// 暂停 3 秒}catch(InterruptedExceptione){e.printStackTrace();}System.out.println(醒了);}}sleep()可以在任何地方调用它让当前线程暂停指定时间但不会释放它持有的任何锁。2.2 wait() 用法publicclassWaitDemo{privatestaticfinalObjectlocknewObject();publicstaticvoidmain(String[]args){synchronized(lock){// 必须要有 synchronizedtry{System.out.println(进入等待...);lock.wait();// 释放 lock 锁进入等待System.out.println(被唤醒继续执行);}catch(InterruptedExceptione){e.printStackTrace();}}}}如果去掉synchronized块运行时会直接抛出Exception in thread main java.lang.IllegalMonitorStateException三、深入监视器wait() 后的线程去哪儿了这是面试中经常追问的点。要回答清楚需要理解 Java 对象监视器Monitor的内部结构。3.1 每个对象都有两个“队列”对于一个被synchronized修饰的对象JVM 会为它维护两个队列队列名称英文存放的线程线程状态竞争队列Entry Set / Contention List想获取锁但还没抢到的线程BLOCKED等待队列Wait Set已经持有锁、但调用wait()主动放弃的线程WAITING3.2 队列流转图┌─────────────────────────────────────┐ │ 竞争队列 (Entry Set) │ │ Thread-B (BLOCKED) │ │ Thread-C (BLOCKED) │ └─────────────────┬───────────────────┘ │ 锁释放后JVM 从竞争队列选一个成为 Owner ▼ ┌─────────────────────────────────────┐ │ Monitor Owner │ │ (当前持有锁的线程) │ └─────────────────┬───────────────────┘ │ owner 调用 wait() 后释放锁 │ ▼ ┌─────────────────────────────────────┐ │ 等待队列 (Wait Set) │ │ Thread-A (WAITING) │ │ Thread-D (WAITING) │ └─────────────────────────────────────┘3.3 关键流转规则竞争队列 → OwnerOwner 释放锁后JVM 从竞争队列中选一个线程获得锁Owner → 等待队列Owner 调用wait()→ 释放锁 → 进入等待队列状态变为WAITING等待队列 → 竞争队列被notify/notifyAll唤醒 → 移入竞争队列状态变为BLOCKED→ 重新参与锁竞争 注意被notify唤醒后线程并不会立即执行它只是从“等待队列”进入了“竞争队列”需要重新竞争锁。竞争成功后才能从wait()方法返回。四、wait/notify 实现线程间通信这是生产者-消费者模式中最经典的协作方式。4.1 完整示例publicclassWaitNotifyCommunication{privatestaticfinalObjectlocknewObject();privatestaticbooleanconditionfalse;// 共享条件// 等待线程staticclassWaiterextendsThread{Overridepublicvoidrun(){synchronized(lock){while(!condition){// ⚠️ 必须用 while防止虚假唤醒try{System.out.println(Thread.currentThread().getName()条件不满足进入等待队列);lock.wait();// 释放锁进入 WAITINGSystem.out.println(Thread.currentThread().getName()被唤醒重新获得锁);}catch(InterruptedExceptione){Thread.currentThread().interrupt();}}System.out.println(Thread.currentThread().getName()条件满足继续执行任务);}}}// 通知线程staticclassNotifierextendsThread{Overridepublicvoidrun(){synchronized(lock){System.out.println(Thread.currentThread().getName()修改条件);conditiontrue;System.out.println(Thread.currentThread().getName()通知等待队列中的线程);lock.notify();// 将等待队列中的一个线程移到竞争队列// 注意此时被唤醒的线程还在竞争队列并未立即执行}// 退出同步块后释放锁被唤醒的线程才能竞争到锁并继续执行}}publicstaticvoidmain(String[]args)throwsInterruptedException{WaiterwaiternewWaiter();waiter.setName(Waiter);NotifiernotifiernewNotifier();notifier.setName(Notifier);waiter.start();Thread.sleep(100);// 确保 waiter 先拿到锁并进入等待notifier.start();}}执行结果Waiter条件不满足进入等待队列 Notifier修改条件 Notifier通知等待队列中的线程 Waiter被唤醒重新获得锁 Waiter条件满足继续执行任务4.2 为什么必须用 while 而不是 if// ❌ 错误写法if(!condition){wait();}// ✅ 正确写法while(!condition){wait();}原因线程被唤醒后条件可能再次不成立比如被另一个线程抢先修改了。用while循环可以重新检查条件这就是所谓的防范虚假唤醒。五、一张表总结对比项sleep()wait()定义位置Thread静态方法Object实例方法所属线程级别对象级别同步要求无必须在synchronized内锁释放不释放释放当前对象锁唤醒自动唤醒需要notify/notifyAll状态变化RUNNABLE→TIMED_WAITINGRUNNABLE→WAITING/TIMED_WAITING典型用途暂停线程、模拟耗时等待条件、线程协作六、思考题如果线程 A 持有锁 lock1然后调用lock1.wait()它会释放 lock1 的锁。但如果它同时还持有另一把锁 lock2lock2 会被释放吗答案不会。wait()只释放当前对象lock1的锁其他锁不受影响。这也是为什么 wait/notify 必须成对使用同一个对象的原因。写在最后sleep()和wait()看似都是让线程暂停但本质上一个作用于线程本身一个作用于对象监视器。理解了这两个方法的区别以及监视器的队列机制Java 并发编程的很多问题都会变得清晰起来。希望这篇文章对你有帮助。如果觉得不错欢迎点赞、收藏、转发
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2491239.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!