一、什么是虚假唤醒虚假唤醒的核心定义很简单线程在没有被 notify()/notifyAll() 唤醒的情况下也可能从 wait() 状态中苏醒过来。就像你在等待一个通知没收到任何消息却自己醒了过来醒来后发现“通知还没来”但程序已经继续执行后续逻辑最终导致错误。这里有两个关键要点必须牢记虚假唤醒是允许的JVM 规范明确说明wait() 方法可能会在没有被通知的情况下返回即虚假唤醒这不是 JVM 的 bug而是为了提升线程调度的效率。虚假唤醒是随机的它可能发生也可能不发生在单线程等待时概率极低但在多线程并发等待时概率会显著增加一旦发生就会导致程序异常。举个通俗的例子你线程A在房间里等待朋友notify()叫你吃饭你睡着了wait()但没等朋友叫你你自己醒了虚假唤醒然后直接去餐厅执行后续逻辑结果发现饭菜还没做好条件不满足导致错误——这就是虚假唤醒的直观场景。二、为什么会发生虚假唤醒底层原因拆解很多人会疑惑为什么 JVM 会允许虚假唤醒这背后是线程调度的底层逻辑核心原因有两个不用深入底层源码理解即可2.1 底层线程调度的“不确定性”JVM 的线程调度依赖于操作系统的线程调度机制而操作系统的线程调度是“抢占式”的且存在各种不可控因素如线程优先级、系统负载、中断等。有时操作系统会在没有收到 notify() 信号的情况下唤醒处于 wait() 状态的线程以此提升线程调度的灵活性和效率。简单说JVM 为了适配不同操作系统的调度逻辑允许 wait() 方法“偶然”苏醒避免线程长期阻塞导致的资源浪费。2.2 多线程竞争下的“信号干扰”在多线程场景中多个线程同时调用 wait() 方法等待同一个锁对象的 notify()/notifyAll() 信号。当其中一个线程被唤醒并执行完毕、释放锁后JVM 可能会“误唤醒”其他等待线程——这些被误唤醒的线程并没有收到有效的通知属于虚假唤醒。比如3个线程A、B、C都在 wait() 等待信号线程D调用 notifyAll() 唤醒它们线程A获取锁并执行完毕此时 JVM 可能会额外唤醒线程B但线程B醒来后条件已经不满足被线程A处理完了这就是典型的虚假唤醒。三、举例说明回到你之前写的 ShareDataOne 类代码这是最典型的“虚假唤醒踩坑案例”——用 if 判断 wait() 的条件看似能正常运行两个线程时但一旦增加线程数量就会出现数据错乱、程序卡死的问题。3.1class ShareDataOne{ private Integer number 0; // 加1方法用if判断条件踩坑点 public synchronized void increment() throws InterruptedException { if (number ! 0) { // 用if判断而非while this.wait(); } number; System.out.println(Thread.currentThread().getName() : number); this.notifyAll(); } // 减1方法同样用if判断踩坑点 public synchronized void decrement() throws InterruptedException { if (number ! 1) { // 用if判断而非while this.wait(); } number--; System.out.println(Thread.currentThread().getName() : number); this.notifyAll(); } }3.2 虚假唤醒导致的问题演示多线程场景当你只有两个线程AAA加1、BBB减1时代码能正常运行因为同一时间只有一个线程在 wait()即使发生虚假唤醒醒来后条件依然满足比如AAA虚假唤醒时number还是0。但如果增加线程数量比如2个加1线程、1个减1线程问题就会暴露线程AAA加1number0执行number变成1notifyAll()释放锁线程BBB减1获取锁number1执行number--变成0notifyAll()释放锁线程CCC加1获取锁number0执行number变成1notifyAll()释放锁此时线程AAA已wait()被虚假唤醒直接跳过if判断因为之前判断过number0执行number导致number变成2预期是1后续线程BBB减1执行时number2不满足if (number ! 1)直接wait()最终所有线程阻塞程序卡死。问题的核心if 判断只执行一次——线程被唤醒无论真实唤醒还是虚假唤醒后不会再次判断条件直接执行后续逻辑导致条件不满足时依然执行出现数据错乱或死锁。四、解决方案用 while 替代 if彻底杜绝虚假唤醒解决虚假唤醒的方案非常简单也是 JVM 官方推荐的做法将 wait() 方法的 if 判断替换成 while 循环判断。核心逻辑线程被唤醒后无论真实还是虚假会再次进入 while 循环重新判断条件如果条件不满足就再次调用 wait() 阻塞直到收到有效的 notify() 信号且条件满足才会执行后续逻辑。3.1 修复后的代码你的代码修改版class ShareDataOne{ private Integer number 0; // 修复if → while public synchronized void increment() throws InterruptedException { // 循环判断被唤醒后再次检查条件 while (number ! 0) { this.wait(); } number; System.out.println(Thread.currentThread().getName() : number); this.notifyAll(); } // 修复if → while public synchronized void decrement() throws InterruptedException { // 循环判断被唤醒后再次检查条件 while (number ! 1) { this.wait(); } number--; System.out.println(Thread.currentThread().getName() : number); this.notifyAll(); } }3.2 修复后为什么能避免虚假唤醒结合之前的多线程场景修复后流程如下线程AAA加1虚假唤醒后进入 while (number ! 0) 判断此时 number1已被线程CCC修改条件满足再次调用 wait() 阻塞不会执行后续的 number直到线程BBB减1执行number变成0调用 notifyAll() 唤醒线程AAA线程AAA再次进入 while 判断number0条件不满足退出循环执行 number逻辑正常。简单说while 循环相当于给线程加了“双重保险”——无论线程是被真实唤醒还是虚假唤醒都会重新检查条件确保只有条件满足时才会执行后续操作彻底杜绝虚假唤醒的影响。
虚假唤醒举例
一、什么是虚假唤醒虚假唤醒的核心定义很简单线程在没有被 notify()/notifyAll() 唤醒的情况下也可能从 wait() 状态中苏醒过来。就像你在等待一个通知没收到任何消息却自己醒了过来醒来后发现“通知还没来”但程序已经继续执行后续逻辑最终导致错误。这里有两个关键要点必须牢记虚假唤醒是允许的JVM 规范明确说明wait() 方法可能会在没有被通知的情况下返回即虚假唤醒这不是 JVM 的 bug而是为了提升线程调度的效率。虚假唤醒是随机的它可能发生也可能不发生在单线程等待时概率极低但在多线程并发等待时概率会显著增加一旦发生就会导致程序异常。举个通俗的例子你线程A在房间里等待朋友notify()叫你吃饭你睡着了wait()但没等朋友叫你你自己醒了虚假唤醒然后直接去餐厅执行后续逻辑结果发现饭菜还没做好条件不满足导致错误——这就是虚假唤醒的直观场景。二、为什么会发生虚假唤醒底层原因拆解很多人会疑惑为什么 JVM 会允许虚假唤醒这背后是线程调度的底层逻辑核心原因有两个不用深入底层源码理解即可2.1 底层线程调度的“不确定性”JVM 的线程调度依赖于操作系统的线程调度机制而操作系统的线程调度是“抢占式”的且存在各种不可控因素如线程优先级、系统负载、中断等。有时操作系统会在没有收到 notify() 信号的情况下唤醒处于 wait() 状态的线程以此提升线程调度的灵活性和效率。简单说JVM 为了适配不同操作系统的调度逻辑允许 wait() 方法“偶然”苏醒避免线程长期阻塞导致的资源浪费。2.2 多线程竞争下的“信号干扰”在多线程场景中多个线程同时调用 wait() 方法等待同一个锁对象的 notify()/notifyAll() 信号。当其中一个线程被唤醒并执行完毕、释放锁后JVM 可能会“误唤醒”其他等待线程——这些被误唤醒的线程并没有收到有效的通知属于虚假唤醒。比如3个线程A、B、C都在 wait() 等待信号线程D调用 notifyAll() 唤醒它们线程A获取锁并执行完毕此时 JVM 可能会额外唤醒线程B但线程B醒来后条件已经不满足被线程A处理完了这就是典型的虚假唤醒。三、举例说明回到你之前写的 ShareDataOne 类代码这是最典型的“虚假唤醒踩坑案例”——用 if 判断 wait() 的条件看似能正常运行两个线程时但一旦增加线程数量就会出现数据错乱、程序卡死的问题。3.1class ShareDataOne{ private Integer number 0; // 加1方法用if判断条件踩坑点 public synchronized void increment() throws InterruptedException { if (number ! 0) { // 用if判断而非while this.wait(); } number; System.out.println(Thread.currentThread().getName() : number); this.notifyAll(); } // 减1方法同样用if判断踩坑点 public synchronized void decrement() throws InterruptedException { if (number ! 1) { // 用if判断而非while this.wait(); } number--; System.out.println(Thread.currentThread().getName() : number); this.notifyAll(); } }3.2 虚假唤醒导致的问题演示多线程场景当你只有两个线程AAA加1、BBB减1时代码能正常运行因为同一时间只有一个线程在 wait()即使发生虚假唤醒醒来后条件依然满足比如AAA虚假唤醒时number还是0。但如果增加线程数量比如2个加1线程、1个减1线程问题就会暴露线程AAA加1number0执行number变成1notifyAll()释放锁线程BBB减1获取锁number1执行number--变成0notifyAll()释放锁线程CCC加1获取锁number0执行number变成1notifyAll()释放锁此时线程AAA已wait()被虚假唤醒直接跳过if判断因为之前判断过number0执行number导致number变成2预期是1后续线程BBB减1执行时number2不满足if (number ! 1)直接wait()最终所有线程阻塞程序卡死。问题的核心if 判断只执行一次——线程被唤醒无论真实唤醒还是虚假唤醒后不会再次判断条件直接执行后续逻辑导致条件不满足时依然执行出现数据错乱或死锁。四、解决方案用 while 替代 if彻底杜绝虚假唤醒解决虚假唤醒的方案非常简单也是 JVM 官方推荐的做法将 wait() 方法的 if 判断替换成 while 循环判断。核心逻辑线程被唤醒后无论真实还是虚假会再次进入 while 循环重新判断条件如果条件不满足就再次调用 wait() 阻塞直到收到有效的 notify() 信号且条件满足才会执行后续逻辑。3.1 修复后的代码你的代码修改版class ShareDataOne{ private Integer number 0; // 修复if → while public synchronized void increment() throws InterruptedException { // 循环判断被唤醒后再次检查条件 while (number ! 0) { this.wait(); } number; System.out.println(Thread.currentThread().getName() : number); this.notifyAll(); } // 修复if → while public synchronized void decrement() throws InterruptedException { // 循环判断被唤醒后再次检查条件 while (number ! 1) { this.wait(); } number--; System.out.println(Thread.currentThread().getName() : number); this.notifyAll(); } }3.2 修复后为什么能避免虚假唤醒结合之前的多线程场景修复后流程如下线程AAA加1虚假唤醒后进入 while (number ! 0) 判断此时 number1已被线程CCC修改条件满足再次调用 wait() 阻塞不会执行后续的 number直到线程BBB减1执行number变成0调用 notifyAll() 唤醒线程AAA线程AAA再次进入 while 判断number0条件不满足退出循环执行 number逻辑正常。简单说while 循环相当于给线程加了“双重保险”——无论线程是被真实唤醒还是虚假唤醒都会重新检查条件确保只有条件满足时才会执行后续操作彻底杜绝虚假唤醒的影响。