小故事引入
- 由于条件不满足,小南不能继续进行计算 但小南如果一直占用着锁,其它人就得一直阻塞,效率太低

- 于是老王单开了一间休息室(调用 wait 方法),让小南到休息室(WaitSet)等着去了,但这时锁释放开,其它人可以由老王随机安排进屋
- 直到小M将烟送来,大叫一声 [ 你的烟到了 ] (调用 notify 方法)

- 小南于是可以离开休息室,重新进入竞争锁的队列

在上面的小故事中,我们有下面几点需要注意的:
- Owner 线程如果发现执行条件不足,可以调用 Monitor 对象对应的 Java 对象的 wait 方法,则它就会进入到 WaitSet 列表,线程状态变为 WAITING
- 位于 EntryList 和 WaitSet 列表的线程都处于阻塞状态,只是从Java层面的线程状态不同,二者都不占用 CPU 时间片
- BLOCKED 线程会在 Owner 线程释放锁的时候被唤醒,至于下一个 Owner 是谁,则由操作系统决定
- 如果当前的 Owner 线程调用了该 Monitor 对象对应的 Java 对象的 notify 时,就会随机唤醒一个位于 WaitSet 的线程进入 EnteryList 等待 CPU 分配时间片
注意:
- 还有一个有参的 wait 方法,它是有时间限制的进入 WaitSet 列表,对应Java层面的线程状态就是TIME_WAITING,时间一过就进入EntryList列表
- 还有一个 notifyAll 方法,它是唤醒位于 WaitSet 列表里面的所有线程,使他们都进入到EntryList列表
举个小例子:
@Slf4j(topic = "c.Test")public class Test {final static Object obj = new Object();public static void main(String[] args) throws InterruptedException {new Thread(() -> {synchronized (obj) {log.debug("执行....");try {obj.wait(); // 让线程在obj上一直等待下去} catch (InterruptedException e) {e.printStackTrace();}log.debug("其它代码....");}},"t1").start();new Thread(() -> {synchronized (obj) {log.debug("执行....");try {obj.wait(); // 让线程在obj上一直等待下去} catch (InterruptedException e) {e.printStackTrace();}log.debug("其它代码....");}},"t2").start();// 主线程两秒后执行Thread.sleep(2000);log.debug("唤醒 obj 上其它线程");synchronized (obj) {obj.notify(); // 唤醒obj上一个线程// obj.notifyAll(); // 唤醒obj上所有等待线程}}}
先来看看调用 obj.notify() 的运行结果:
可以看到唤醒了t1线程,但是程序没有结束,因为t2还在无时间限制的wait。再看看调用 obj.notifyAll() 的运行结果:
可以看到t1、t2被同时唤醒,并且程序正常退出
wait和sleep的区别
wait和sleep虽然都可以让线程进入阻塞,但是二者有着很多并且具有本质性的区别,主要可以从一下几个维度分析:
所属类
wait 方法是 Object 类的方法,所有的类都会具有;但是 sleep 方法是 Thread 类的方法。但是二者都是 native 本地方法,前者是 final 修饰的 后者是静态方法,如下图所示:
使用限制
- 使用 sleep 方法可以让当前线程进入睡眠状态,时间一到线程会继续运行下去,在任何时候都可以调用,只需要处理或抛出对应的 InterruptedException 异常即可:

- wait方法只能在同步代码块(synchronized代码块)里面调用,同样也需要处理或抛出 InterruptedException 异常。至于其原因可以参考wait-notify的工作原理。
使用效果
在线程中调用某对象的wait方法,会释放当前线程对该对象持有的锁,但是sleep则不会释放锁。并且sleep 会让出 CPU 执行时间且强制上下文切换,而 wait 则不一定,wait 后可能还是有机会重新竞争到锁继续执行的。
wait-notify机制的正确使用
steep one
@Slf4j(topic = "c.Test")public class Test {static final Object room = new Object();static boolean hasCigarette = false;public static void main(String[] args) throws InterruptedException {new Thread(() -> {synchronized (room) {log.debug("有烟没?[{}]", hasCigarette);if (!hasCigarette) {log.debug("没烟,歇一会先");try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}log.debug("有烟没?[{}]", hasCigarette);if (hasCigarette) {log.debug("可以开始干活了");} else {log.debug("活没有干成");}}}, "小南").start();for (int i = 0; i < 5; i++) {new Thread(() -> {synchronized (room) {log.debug("可以开始干活了");}}, "其它人").start();}Thread.sleep(1000);new Thread(() -> {// 这里能不能加 synchronized (room)?hasCigarette = true;log.debug("烟到了噢!");}, "送烟的").start();}}
运行结果:
可以看到,如果在同步代码块中使用sleep进入等待,一定阻塞其它线程,会影响到并发效率。为了解决这个问题,可以采用wait-notify机制,如steep two
steep two
修改一下两处代码:
if (!hasCigarette) {log.debug("没烟,歇一会先");try {room.wait(2000);} catch (InterruptedException e) {e.printStackTrace();}}synchronized (room) {hasCigarette = true;log.debug("烟到了噢!");room.notify();}
运行结果:
可以看到。没有再阻塞其它线程。但是,上面只是一个线程再等待条件,如果有两个线程了?
steep three
为了方便测试,将有时间的wait换成无时间的wait,并且加多一个需要条件的线程:
@Slf4j(topic = "c.Test")public class Test {static final Object room = new Object();static boolean hasCigarette = false;static boolean hasTakeout = false;public static void main(String[] args) throws InterruptedException {new Thread(() -> {synchronized (room) {log.debug("有烟没?[{}]", hasCigarette);if (!hasCigarette) {log.debug("没烟,歇一会先");try {room.wait();} catch (InterruptedException e) {e.printStackTrace();}}log.debug("有烟没?[{}]", hasCigarette);if (hasCigarette) {log.debug("可以开始干活了");} else {log.debug("活没有干成");}}}, "小南").start();new Thread(() -> {synchronized (room) {log.debug("有面包没?[{}]", hasCigarette);if (!hasCigarette) {log.debug("没面包,歇一会先");try {room.wait();} catch (InterruptedException e) {e.printStackTrace();}}log.debug("有面包没?[{}]", hasCigarette);if (hasCigarette) {log.debug("可以开始干活了");} else {log.debug("活没有干成");}}}, "小女").start();for (int i = 0; i < 5; i++) {new Thread(() -> {synchronized (room) {log.debug("可以开始干活了");}}, "其它人").start();}Thread.sleep(1000);new Thread(() -> {synchronized (room) {hasTakeout = true;log.debug("面包到了噢!");room.notify();}}, "送面包的").start();}}
可以看到,小南在等烟,小女在等面包,看一下运行结果:
可以看到,面包送到了,但是唤醒的却是需要烟的小南,反而需要面包的小女没有被唤醒,这其实就叫做虚假唤醒,即不能唤醒正确的线程。其解决办法也很简单,就是改用notifyAll即可,如steep four
steep four
修改为:
new Thread(() -> {synchronized (room) {hasTakeout = true;log.debug("面包到了噢!");room.notifyAll();}}, "送面包的").start();

但是新的问题又来了:用 notifyAll 仅解决某个线程的唤醒问题,但使用 if + wait 判断仅有一次机会,一旦条件不成立,就没有重新判断的机会了。一个简单的解决方法就是使用:用 while + wait,当条件不成立,再次 wait
steep five
将if修改为while判断:
while (!hasCigarette) {log.debug("没烟,先歇会!");try {room.wait();} catch (InterruptedException e) {e.printStackTrace();}}
下面就是wait-notify机制正确的使用模板:


