小故事引入

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

image.png

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

image.png

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

image.png

在上面的小故事中,我们有下面几点需要注意的:

  1. 只有进入到 Owner 房间的人(即线程)才有资格进入 WaitSet 等待室
  2. 送烟的人只有进入到 Owner 房间才能叫醒 WaitSet 里面的小南

    wait-notify工作原理

    image.png
    image.png
  • Owner 线程如果发现执行条件不足,可以调用 Monitor 对象对应的 Java 对象的 wait 方法,则它就会进入到 WaitSet 列表,线程状态变为 WAITING
  • 位于 EntryList 和 WaitSet 列表的线程都处于阻塞状态,只是从Java层面的线程状态不同,二者都不占用 CPU 时间片
  • BLOCKED 线程会在 Owner 线程释放锁的时候被唤醒,至于下一个 Owner 是谁,则由操作系统决定
  • 如果当前的 Owner 线程调用了该 Monitor 对象对应的 Java 对象的 notify 时,就会随机唤醒一个位于 WaitSet 的线程进入 EnteryList 等待 CPU 分配时间片

    注意:

    1. 还有一个有参的 wait 方法,它是有时间限制的进入 WaitSet 列表,对应Java层面的线程状态就是TIME_WAITING,时间一过就进入EntryList列表
    2. 还有一个 notifyAll 方法,它是唤醒位于 WaitSet 列表里面的所有线程,使他们都进入到EntryList列表

举个小例子:

  1. @Slf4j(topic = "c.Test")
  2. public class Test {
  3. final static Object obj = new Object();
  4. public static void main(String[] args) throws InterruptedException {
  5. new Thread(() -> {
  6. synchronized (obj) {
  7. log.debug("执行....");
  8. try {
  9. obj.wait(); // 让线程在obj上一直等待下去
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. log.debug("其它代码....");
  14. }
  15. },"t1").start();
  16. new Thread(() -> {
  17. synchronized (obj) {
  18. log.debug("执行....");
  19. try {
  20. obj.wait(); // 让线程在obj上一直等待下去
  21. } catch (InterruptedException e) {
  22. e.printStackTrace();
  23. }
  24. log.debug("其它代码....");
  25. }
  26. },"t2").start();
  27. // 主线程两秒后执行
  28. Thread.sleep(2000);
  29. log.debug("唤醒 obj 上其它线程");
  30. synchronized (obj) {
  31. obj.notify(); // 唤醒obj上一个线程
  32. // obj.notifyAll(); // 唤醒obj上所有等待线程
  33. }
  34. }
  35. }

先来看看调用 obj.notify() 的运行结果:
image.png
可以看到唤醒了t1线程,但是程序没有结束,因为t2还在无时间限制的wait。再看看调用 obj.notifyAll() 的运行结果:
image.png
可以看到t1、t2被同时唤醒,并且程序正常退出

wait和sleep的区别

wait和sleep虽然都可以让线程进入阻塞,但是二者有着很多并且具有本质性的区别,主要可以从一下几个维度分析:

所属类

wait 方法是 Object 类的方法,所有的类都会具有;但是 sleep 方法是 Thread 类的方法。但是二者都是 native 本地方法,前者是 final 修饰的 后者是静态方法,如下图所示:
image.png

使用限制

  • 使用 sleep 方法可以让当前线程进入睡眠状态,时间一到线程会继续运行下去,在任何时候都可以调用,只需要处理或抛出对应的 InterruptedException 异常即可:

image.png

  • wait方法只能在同步代码块(synchronized代码块)里面调用,同样也需要处理或抛出 InterruptedException 异常。至于其原因可以参考wait-notify的工作原理。

image.png

使用效果

在线程中调用某对象的wait方法,会释放当前线程对该对象持有的锁,但是sleep则不会释放锁。并且sleep 会让出 CPU 执行时间且强制上下文切换,而 wait 则不一定,wait 后可能还是有机会重新竞争到锁继续执行的。
image.png

wait-notify机制的正确使用

steep one

  1. @Slf4j(topic = "c.Test")
  2. public class Test {
  3. static final Object room = new Object();
  4. static boolean hasCigarette = false;
  5. public static void main(String[] args) throws InterruptedException {
  6. new Thread(() -> {
  7. synchronized (room) {
  8. log.debug("有烟没?[{}]", hasCigarette);
  9. if (!hasCigarette) {
  10. log.debug("没烟,歇一会先");
  11. try {
  12. Thread.sleep(2000);
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. }
  17. log.debug("有烟没?[{}]", hasCigarette);
  18. if (hasCigarette) {
  19. log.debug("可以开始干活了");
  20. } else {
  21. log.debug("活没有干成");
  22. }
  23. }
  24. }, "小南").start();
  25. for (int i = 0; i < 5; i++) {
  26. new Thread(() -> {
  27. synchronized (room) {
  28. log.debug("可以开始干活了");
  29. }
  30. }, "其它人").start();
  31. }
  32. Thread.sleep(1000);
  33. new Thread(() -> {
  34. // 这里能不能加 synchronized (room)?
  35. hasCigarette = true;
  36. log.debug("烟到了噢!");
  37. }, "送烟的").start();
  38. }
  39. }

运行结果:
image.png
可以看到,如果在同步代码块中使用sleep进入等待,一定阻塞其它线程,会影响到并发效率。为了解决这个问题,可以采用wait-notify机制,如steep two

steep two

修改一下两处代码:

  1. if (!hasCigarette) {
  2. log.debug("没烟,歇一会先");
  3. try {
  4. room.wait(2000);
  5. } catch (InterruptedException e) {
  6. e.printStackTrace();
  7. }
  8. }
  9. synchronized (room) {
  10. hasCigarette = true;
  11. log.debug("烟到了噢!");
  12. room.notify();
  13. }

运行结果:
image.png
可以看到。没有再阻塞其它线程。但是,上面只是一个线程再等待条件,如果有两个线程了?

steep three

为了方便测试,将有时间的wait换成无时间的wait,并且加多一个需要条件的线程:

  1. @Slf4j(topic = "c.Test")
  2. public class Test {
  3. static final Object room = new Object();
  4. static boolean hasCigarette = false;
  5. static boolean hasTakeout = false;
  6. public static void main(String[] args) throws InterruptedException {
  7. new Thread(() -> {
  8. synchronized (room) {
  9. log.debug("有烟没?[{}]", hasCigarette);
  10. if (!hasCigarette) {
  11. log.debug("没烟,歇一会先");
  12. try {
  13. room.wait();
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. }
  18. log.debug("有烟没?[{}]", hasCigarette);
  19. if (hasCigarette) {
  20. log.debug("可以开始干活了");
  21. } else {
  22. log.debug("活没有干成");
  23. }
  24. }
  25. }, "小南").start();
  26. new Thread(() -> {
  27. synchronized (room) {
  28. log.debug("有面包没?[{}]", hasCigarette);
  29. if (!hasCigarette) {
  30. log.debug("没面包,歇一会先");
  31. try {
  32. room.wait();
  33. } catch (InterruptedException e) {
  34. e.printStackTrace();
  35. }
  36. }
  37. log.debug("有面包没?[{}]", hasCigarette);
  38. if (hasCigarette) {
  39. log.debug("可以开始干活了");
  40. } else {
  41. log.debug("活没有干成");
  42. }
  43. }
  44. }, "小女").start();
  45. for (int i = 0; i < 5; i++) {
  46. new Thread(() -> {
  47. synchronized (room) {
  48. log.debug("可以开始干活了");
  49. }
  50. }, "其它人").start();
  51. }
  52. Thread.sleep(1000);
  53. new Thread(() -> {
  54. synchronized (room) {
  55. hasTakeout = true;
  56. log.debug("面包到了噢!");
  57. room.notify();
  58. }
  59. }, "送面包的").start();
  60. }
  61. }

可以看到,小南在等烟,小女在等面包,看一下运行结果:
image.png
可以看到,面包送到了,但是唤醒的却是需要烟的小南,反而需要面包的小女没有被唤醒,这其实就叫做虚假唤醒,即不能唤醒正确的线程。其解决办法也很简单,就是改用notifyAll即可,如steep four

steep four

修改为:

  1. new Thread(() -> {
  2. synchronized (room) {
  3. hasTakeout = true;
  4. log.debug("面包到了噢!");
  5. room.notifyAll();
  6. }
  7. }, "送面包的").start();

image.png
但是新的问题又来了:用 notifyAll 仅解决某个线程的唤醒问题,但使用 if + wait 判断仅有一次机会,一旦条件不成立,就没有重新判断的机会了。一个简单的解决方法就是使用:用 while + wait,当条件不成立,再次 wait

steep five

将if修改为while判断:

  1. while (!hasCigarette) {
  2. log.debug("没烟,先歇会!");
  3. try {
  4. room.wait();
  5. } catch (InterruptedException e) {
  6. e.printStackTrace();
  7. }
  8. }

下面就是wait-notify机制正确的使用模板:
image.png