一、原理
分析
- Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
- BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
- BLOCKED 线程会在 Owner 线程释放锁时唤醒
- WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList 重新竞争
二、API介绍
方法 |
说明 |
obj.wait() |
wait方法让进入object监视器的线程到waitSet等待。wait后会释放对象锁,让其他线程竞争。 |
obj.wait(Long timeout) |
限时等待。导致当前的线程等待,等待被其他线程唤醒,或者指定的时间timeout用完,线程不再等待。 |
obj.notify() |
在 object 上正在 等待的线程中挑一个唤醒。 |
obj.notifyAll() |
让 object 上正在 等待的线程全部唤醒。 |
它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法。
示例代码
@Slf4j(topic = "c.TestWaitNotify") public class TestWaitNotify { final static Object obj = new Object(); public static void main(String[] args) { 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(); //主线程两秒后执行 sleep(2); log.debug("唤醒 obj 上其它线程"); synchronized (obj) { obj.notify(); // 唤醒obj上一个线程 // obj.notifyAll(); // 唤醒obj上所有等待线程 } } }
- notify运行结果
22:36:43.159 c.TestWaitNotify [t1] - 执行... 22:36:43.161 c.TestWaitNotify [t2] - 执行... 22:36:45.171 c.TestWaitNotify [main] - 唤醒 obj 上其它线程 22:36:45.171 c.TestWaitNotify [t1] - 其它代码...
- notifyAll运行结果
22:39:49.383 c.TestWaitNotify [t1] - 执行... 22:39:49.385 c.TestWaitNotify [t2] - 执行... 22:39:51.389 c.TestWaitNotify [main] - 唤醒 obj 上其它线程 22:39:51.389 c.TestWaitNotify [t2] - 其它代码... 22:39:51.389 c.TestWaitNotify [t1] - 其它代码...
wait()
方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到 notify 为止。
wait(long n)
有时限的等待, 到 n 毫秒后结束等待,或是被 notify。
三、wait notify的正确姿势
3.1 sleep(long n) 和 wait(long n) 的区别
- sleep 是 Thread 的方法,而 wait 是 Object 的方法
- sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
- sleep 在睡眠的同时,不会释放对象锁,但 wait 在等待的时候会释放对象锁
- 无时限wait方法执行后线程变为WAITING状态,有时限的wait方法与sleep方法执行后变为TIMED_WAITING状态
3.2 演变步骤
step 1
@Slf4j(topic = "c.TestCorrectPostureStep1") public class TestCorrectPostureStep1 { static final Object room = new Object(); static boolean hasCigarette = false; //有没有烟 static boolean hasTakeout = false; public static void main(String[] args) { new Thread(() -> { synchronized (room) { log.debug("有烟没?[{}]",hasCigarette); if(!hasCigarette){ log.debug("没烟,先歇会!"); sleep(2); } log.debug("有烟没?[{}]",hasCigarette); if (hasCigarette){ log.debug("可以开始干活了"); } } },"Leefs").start(); for (int i = 0; i < 5; i++){ new Thread(() -> { synchronized (room) { log.debug("可以开始干活了"); } },"其他人").start(); } sleep(1); new Thread(() -> { synchronized (room) { hasCigarette = true; log.debug("烟到了!"); } },"送烟的").start(); } }
运行结果
22:50:16.626 c.TestCorrectPostureStep1 [Leefs] - 有烟没?[false] 22:50:16.628 c.TestCorrectPostureStep1 [Leefs] - 没烟,先歇会! ---------- 中间间隔2s ------- 22:50:18.638 c.TestCorrectPostureStep1 [Leefs] - 有烟没?[false] 22:50:18.638 c.TestCorrectPostureStep1 [送烟的] - 烟到了! 22:50:18.638 c.TestCorrectPostureStep1 [其他人] - 可以开始干活了 22:50:18.638 c.TestCorrectPostureStep1 [其他人] - 可以开始干活了 22:50:18.638 c.TestCorrectPostureStep1 [其他人] - 可以开始干活了 22:50:18.639 c.TestCorrectPostureStep1 [其他人] - 可以开始干活了 22:50:18.639 c.TestCorrectPostureStep1 [其他人] - 可以开始干活了
分析
- 通过sleep方法来使线程处于等待状态;
- 【Leefs线程】必须睡足2s后才能醒来,就算烟提前送到,也无法立刻醒来;
- 如果送烟的线程也加了 synchronized (room) 后,就好比【Leefs】在里面反锁了门睡觉,烟根本没法送进门,main 没加synchronized 就好像 main 线程是翻窗户进来的;
- 当【Leefs线程】获得锁在睡眠时,其它干活的线程,都要一直阻塞,效率太低;
- 解决方法,使用 wait - notify 机制。
step 2
@Slf4j(topic = "c.TestCorrectPostureStep2") public class TestCorrectPostureStep2 { static final Object room = new Object(); static boolean hasCigarette = false; static boolean hasTakeout = false; public static void main(String[] args) { new Thread(() -> { synchronized (room) { log.debug("有烟没?[{}]",hasCigarette); if(!hasCigarette){ log.debug("没烟,先歇会!"); try { room.wait(2000); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("有烟没?[{}]", hasCigarette); if(hasCigarette){ log.debug("可以开始干活了"); } } },"Leefs").start(); for (int i = 0; i < 5; i++){ new Thread(() -> { synchronized (room){ log.debug("可以开始干活了"); } },"其他人").start(); } sleep(1); new Thread(() -> { synchronized (room){ hasCigarette = true; log.debug("烟到了!"); room.notify(); } },"送烟的").start(); } }
运行结果
23:05:18.333 c.TestCorrectPostureStep2 [Leefs] - 有烟没?[false] 23:05:18.336 c.TestCorrectPostureStep2 [Leefs] - 没烟,先歇会! 23:05:18.336 c.TestCorrectPostureStep2 [其他人] - 可以开始干活了 23:05:18.336 c.TestCorrectPostureStep2 [其他人] - 可以开始干活了 23:05:18.336 c.TestCorrectPostureStep2 [其他人] - 可以开始干活了 23:05:18.336 c.TestCorrectPostureStep2 [其他人] - 可以开始干活了 23:05:18.336 c.TestCorrectPostureStep2 [其他人] - 可以开始干活了 23:05:19.342 c.TestCorrectPostureStep2 [送烟的] - 烟到了! 23:05:19.343 c.TestCorrectPostureStep2 [Leefs] - 有烟没?[true] 23:05:19.343 c.TestCorrectPostureStep2 [Leefs] - 可以开始干活了
- 解决了当【Leefs线程】处于睡眠状态时,其他线程可以获取锁不需等待释放锁的问题
- 但如果有其它线程也在等待条件呢?送烟的主线程notify会不会错误地叫醒其他线程呢?
step 3
@Slf4j(topic = "c.TestCorrectPostureStep3") public class TestCorrectPostureStep3 { static final Object room = new Object(); static boolean hasCigarette = false; static boolean hasTakeout = false; public static void main(String[] args) { 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("没干成活..."); } } },"Leefs").start(); sleep(1); new Thread(() -> { synchronized (room){ log.debug("外卖送到没?[{}]",hasTakeout); if(!hasTakeout){ log.debug("没外卖,先歇会!"); try { room.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("外卖送到没?[{}]", hasTakeout); if (hasTakeout) { log.debug("可以开始干活了"); } else { log.debug("没干成活..."); } } },"Jeyoo").start(); sleep(1); new Thread(() -> { synchronized (room) { hasTakeout = true; log.debug("外卖送到了!"); room.notify(); } },"送外卖的").start(); } }
运行结果
23:12:26.760 c.TestCorrectPostureStep3 [Leefs] - 有烟没?[false] 23:12:26.762 c.TestCorrectPostureStep3 [Leefs] - 没烟,先歇会! 23:12:27.766 c.TestCorrectPostureStep3 [Jeyoo] - 外卖送到没?[false] 23:12:27.766 c.TestCorrectPostureStep3 [Jeyoo] - 没外卖,先歇会! 23:12:28.771 c.TestCorrectPostureStep3 [送外卖的] - 外卖送到了! 23:12:28.772 c.TestCorrectPostureStep3 [Leefs] - 有烟没?[false] 23:12:28.773 c.TestCorrectPostureStep3 [Leefs] - 没干成活...
- notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程,称之为【虚假唤醒】
- 解决方法,改为 notifyAll
step 4
@Slf4j(topic = "c.TestCorrectPostureStep4") public class TestCorrectPostureStep4 { static final Object room = new Object(); static boolean hasCigarette = false; static boolean hasTakeout = false; public static void main(String[] args) { 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("没干成活..."); } } },"Leefs").start(); sleep(1); new Thread(() -> { synchronized (room){ log.debug("外卖送到没?[{}]",hasTakeout); if(!hasTakeout){ log.debug("没外卖,先歇会!"); try { room.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("外卖送到没?[{}]", hasTakeout); if (hasTakeout) { log.debug("可以开始干活了"); } else { log.debug("没干成活..."); } } },"Jeyoo").start(); sleep(1); new Thread(() -> { synchronized (room) { hasTakeout = true; log.debug("外卖送到了!"); room.notifyAll(); } },"送外卖的").start(); } }
运行结果
10:42:37.796 c.TestCorrectPostureStep5 [Leefs] - 有烟没?[false] 10:42:37.798 c.TestCorrectPostureStep5 [Leefs] - 没烟,先歇会! 10:42:38.801 c.TestCorrectPostureStep5 [Jeyoo] - 外卖送到没?[false] 10:42:38.801 c.TestCorrectPostureStep5 [Jeyoo] - 没外卖,先歇会! 10:42:39.809 c.TestCorrectPostureStep5 [送外卖的] - 外卖送到了! 10:42:39.810 c.TestCorrectPostureStep5 [Jeyoo] - 外卖送到没?[true] 10:42:39.810 c.TestCorrectPostureStep5 [Jeyoo] - 可以开始干活了 10:42:39.810 c.TestCorrectPostureStep5 [Leefs] - 没烟,先歇会!
- 用 notifyAll 仅解决某个线程的唤醒问题,但使用 if + wait 判断仅有一次机会,一旦条件不成立,就没有重新判断的机会了
- 解决方法,用 while + wait,当条件不成立,再次 wait
step 5
将 if 改为 while
if (!hasCigarette) { log.debug("没烟,先歇会!"); try { room.wait(); } catch (InterruptedException e) { e.printStackTrace(); } }
改动后
- wait方法被唤醒后才会执行后面代码,因此此处不会导致while循环空转
while (!hasCigarette) { log.debug("没烟,先歇会!"); try { room.wait(); } catch (InterruptedException e) { e.printStackTrace(); } }
运行结果
23:20:07.016 c.TestCorrectPostureStep5 [Leefs] - 有烟没?[false] 23:20:07.019 c.TestCorrectPostureStep5 [Leefs] - 没烟,先歇会! 23:20:08.030 c.TestCorrectPostureStep5 [Jeyoo] - 外卖送到没?[false] 23:20:08.031 c.TestCorrectPostureStep5 [Jeyoo] - 没外卖,先歇会! 23:20:09.033 c.TestCorrectPostureStep5 [送外卖的] - 外卖送到了! 23:20:09.034 c.TestCorrectPostureStep5 [Jeyoo] - 外卖送到没?[true] 23:20:09.034 c.TestCorrectPostureStep5 [Jeyoo] - 可以开始干活了 23:20:09.034 c.TestCorrectPostureStep5 [Leefs] - 没烟,先歇会!
总结:正确姿势
synchronized(lock) { while(条件不成立) { lock.wait(); } // 干活 } //另一个线程 synchronized(lock) { lock.notifyAll(); }