java多线程 - wait/notify

192 阅读4分钟

系列章节

  1. java多线程-基础
  2. Java多线程 - Synchronized

wait/notify机制用于在多线程处理时,某个线程条件未满足进入等待,其他线程改变条件后通知其才能继续运行。

背景

有一个房间,张三和李四要进去干活,为了避免冲突每次只能进一个人干活,但是张三干活时需要叼根烟,李四干活时需要喝点水,不然两人就干不了活。

例1-wait/notify

只送了水,没送烟。如下代码所示:

 private final static Object room = new Object();
 ​
 private static boolean hasWater;
 ​
 public static void main(String[] args) {
     Thread t1 = new Thread(() -> {
         synchronized (room) {
             log.debug("我需要根烟...");
 ​
             // 没烟, 进入等待
             if (!hasCigarette) {
                 try {
                     // 等待烟
                     room.wait();
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             }
 ​
             if (hasCigarette) {
                 // 烟到了
                 log.debug("烟到了, 继续干活");
             } else {
                 // 烟没到, 被虚假唤醒
                 log.debug("烟没到, 不干活了");
             }
         }
 ​
     }, "张三");
 ​
     Thread t2 = new Thread(() -> {
         synchronized (room) {
             log.debug("我需要点水...");
 ​
             // 没水, 进入等待
             if (!hasWater) {
                 try {
                     // 等待水
                     room.wait();
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             }
 ​
             if (hasWater) {
                 // 水到了
                 log.debug("水到了, 继续干活");
             } else {
                 // 水没到, 被虚假唤醒
                 log.debug("水没到, 不干活了");
             }
         }
     }, "李四");
     
     t1.start();
     t2.start();
     // t2.start();
     // t1.start();
 ​
     // 等1秒, 让t1,t2先运行
     sleep(1);
 ​
     // 送水
     hasWater = true;
     synchronized (room) {
         // 通知等待线程
         room.notify();
     }
 ​
 }

运行结果如下:

  1. t1先启动,t2后启动

image.png

  1. t2先启动,t1后启动

image.png

notify()是随机唤醒一个阻塞的线程,所以会出现上述两种情况。主要是激活的条件和唤醒的线程不一致(虚假唤醒),接下来使用notifyAll()解决这个问题。

例2-wait/notifyAll

只送了水,没送烟。为避免虚假唤醒使用notifyAll(),如下代码所示:

 private final static Object room = new Object();
 private static boolean hasCigarette;
 private static boolean hasWater;
 ​
 public static void main(String[] args) {
     Thread t1 = new Thread(() -> {
         synchronized (room) {
             log.debug("我需要根烟...");
 ​
             // 没烟, 进入等待
             if (!hasCigarette) {
                 try {
                     // 等待烟
                     room.wait();
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             }
 ​
             if (hasCigarette) {
                 // 烟到了
                 log.debug("烟到了, 继续干活");
             } else {
                 // 烟没到, 被虚假唤醒
                 log.debug("烟没到, 不干活了");
             }
         }
 ​
     }, "张三");
 ​
     Thread t2 = new Thread(() -> {
         synchronized (room) {
             log.debug("我需要点水...");
 ​
             // 没水, 进入等待
             if (!hasWater) {
                 try {
                     // 等待水
                     room.wait();
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             }
 ​
             if (hasWater) {
                 // 水到了
                 log.debug("水到了, 继续干活");
             } else {
                 // 水没到, 被虚假唤醒
                 log.debug("水没到, 不干活了");
             }
         }
     }, "李四");
     
     t1.start();
     t2.start();
     // t2.start();
     // t1.start();
 ​
     // 等1秒, 让t1,t2先运行
     sleep(1);
 ​
     // 送水
     hasWater = true;
     synchronized (room) {
         // 通知等待线程
         room.notifyAll();
     }
 ​
 }

运行结果如下:

image.png

因为使用了notifyAll(),上述代码执行完后,李四线程顺利执行完成,但是张三线程条件不满足,最终没干成活。我们期望的应该是就算条件不满足也应该继续等待条件满足后干完活再结束线程。

例3-wait/notifyAll/while

送水送烟,但是送的时间不一样。而且条件判断的if改成while

 private final static Object room = new Object();
 private static boolean hasCigarette;
 private static boolean hasWater;
 ​
 public static void main(String[] args) {
     Thread t1 = new Thread(() -> {
         synchronized (room) {
             log.debug("我需要根烟...");
 ​
             // 没烟, 进入等待
             while (!hasCigarette) {
                 try {
                     // 等待烟
                     room.wait();
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             }
 ​
             if (hasCigarette) {
                 // 烟到了
                 log.debug("烟到了, 继续干活");
             } else {
                 // 烟没到, 被虚假唤醒
                 log.debug("烟没到, 不干活了");
             }
         }
 ​
     }, "张三");
 ​
     Thread t2 = new Thread(() -> {
         synchronized (room) {
             log.debug("我需要点水...");
 ​
             // 没水, 进入等待
             while (!hasWater) {
                 try {
                     // 等待水
                     room.wait();
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             }
 ​
             if (hasWater) {
                 // 水到了
                 log.debug("水到了, 继续干活");
             } else {
                 // 水没到, 被虚假唤醒
                 log.debug("水没到, 不干活了");
             }
         }
     }, "李四");
 ​
     t1.start();
     t2.start();
 ​
     // 等1秒, 让t1,t2先运行
     sleep(1);
 ​
     // 先改变水的条件
     hasWater = true;
     synchronized (room) {
         // 通知等待线程
         room.notifyAll();
     }
 ​
     // 再等1秒,改变烟的条件
     sleep(1);
     hasCigarette = true;
     synchronized (room) {
         // 通知等待线程
         room.notifyAll();
     }
 }

运行结果如下:

image.png

线程启动后,过了1秒,改变了水的条件,所以李四线程继续干活了,因我们使用的是notifyAll(),所以此时张三线程被虚假唤醒了,但是因为我们使用了while来判断条件,条件不满足,其又等待了一次。再过1秒,改变了烟的条件,所以张三线程继续干活了。

所以最终我们得出了使用wait/notifyAll的最佳姿势,模板代码如下:

 // 干活线程
 synchronized(lock) {
     while(条件不成立) {
         lock.wait();
     }
     // 继续干活
 }
 // 通知线程
 synchronized(lock) {
     lock.notifyAll();
 }

wait/notify原理

上一章我们讲synchronized原理时提到了重量级锁使用的Monitor(了解Monitor请查看上一章),wait/notify也是使用Monitor机制来实现的,如下图所示:

image.png

  1. t0,t1作为Owner时,调用了wait()就会进入WaitSet区域,这时其会释放锁,并唤醒EntryList中的线程
  2. t2通过竞争锁作为Owner时,如果调用了notifyAll(),那t0,t1就会被唤醒重新进入EntryList来竞争锁

总结

  • wait/notify是基于Monitor的实现方式,所以其必须放在synchronized块中才会正常运行,如果不放在synchronized块中则会报错,如下图:

image.png

  • 为避免虚假唤醒,需使用wait/notifyAll/while方式来正确使用。

  • 因基于Monitor的实现方式,所以其效率不是很高,后续可采用ReentrantLock来优化。