前言
本文主要讲述一个概念:虚假唤醒(spurious wakeup)。
在并发编程中,我们可能在实践中并没有遇到过,但是它确实存在,概率较低,但一旦出现,问题就非常的大。
比如我们给方法上锁,经常会使用到this.wait()的方式,但是此方法JDK官方在doc文档里已经给我们说明了:它是有可能出现虚假唤醒现象的,如下截图我是在JDK官方的doc文档截的
大致的意思如下:
线程也可以在不被通知、中断或超时的情况下唤醒,即所谓的虚假唤醒。虽然这在实践中很少发生,但是应用程序必须通过测试导致线程被唤醒的条件来防止这种情况,并且如果条件不满足则继续等待。换句话说,等待应该总是在循环中发生。
代码示例
现在通过代码的方式,来演示出什么叫做虚假唤醒,这样能够更好的理解
先构建三个类:店员 生产者 消费者
// 店员类:负责进货和售货 class Clerk{ //TOTAL表示我的店最大可以容纳的总量 private static final int TOTAL=1; //数字取1是为了放大问题 private int num=0; //店里当前的货物量 public synchronized void get() { //店员进货 每次进货一个 if(num >= TOTAL) { System.out.println("库存已满,无法进货"); try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } else { System.out.println(Thread.currentThread().getName()+" : "+ (num++)); this.notifyAll(); } } public synchronized void sale() { //店员卖货 每次卖掉一个货 if(num<=0) { System.out.println("库存已空,无法卖货"); try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } }else { System.out.println(Thread.currentThread().getName()+" : "+(num--)); this.notifyAll(); } } } // 生产者 可以有很多生产者卖货给这个店员 class Producer implements Runnable{ private Clerk clerk; public Producer(Clerk clerk) { this.clerk=clerk; } @Override public void run() { for (int i = 0; i<20; i++) { try { Thread.sleep(200); //放大问题 } catch (InterruptedException e) { e.printStackTrace(); } clerk.get(); } } } //消费者:可以很多消费者找店员买货 class Consumer implements Runnable{ private Clerk clerk; public Consumer(Clerk clerk) { this.clerk=clerk; } @Override public void run() { for (int i = 0; i<20; i++) { clerk.sale(); } } }
先我们只用一个生产者,一个消费者试试:
public static void main(String[] args) { Clerk clerk=new Clerk(); Producer producer=new Producer(clerk); Consumer consumer=new Consumer(clerk); new Thread(producer,"生产者A1").start(); new Thread(consumer,"消费者B1").start(); }
各位读者可以先看看代码,猜猜结果。
代码咋一看,其实真的没什么问题,但是因为我们通过sleep把问题放大了,所以我们运行一下,**竟然发现控制台一直都没有结束。**而我们这里是for20次循环,按理来说程序最终会终止,可情况恰恰相反。
问题分析
分析产生上面控制台一直都没有停的原因:
问题产生的根源是,由于生产者现象睡眠了200毫秒,因而可能产生的情况是最后消费者线程循环走完了然后就真的结束了,而生产者线程由于wait没有线程来唤醒,所以最终导致一直等待,因而程序不会结束,控制台就不终止。
解决方案:
一:把同步方法的else去掉了,那么无论最终哪个线程先走完,都会执行wait后面的方法,即它在结束前会唤醒等待的线程,因而这个线程最终也会完整的执行完,最后程序终止。
if(num<=0) { System.out.println("库存已空,无法卖货"); try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName()+" : "+(num--)); this.notifyAll(); ... //另外一个同步方法等其余代码省略
这种办法貌似解决了我们的问题,程序也正常的终止了。但是我们再加一组生产者、消费者试试:
public static void main(String[] args) { Clerk clerk=new Clerk(); Producer producer=new Producer(clerk); Consumer consumer=new Consumer(clerk); Producer producer2=new Producer(clerk); Consumer consumer2=new Consumer(clerk); new Thread(producer,"生产者A1").start(); new Thread(consumer,"消费者B1").start(); new Thread(producer2,"生产者A2").start(); new Thread(consumer2,"消费者B2").start(); }
运行,竟然发现出现了产品为负数的情况。这,就,尴尬了。肯定不合适。因为我们把esle放开了,所以每次都notifyAll()出现了虚假唤醒现象。
因为有可能num==0,然后两个消费者线程都wait,此时生产者执行num++后,在唤醒却是唤醒了所有等待的线程,此时这两个消费者线程抢占资源后立马执行wait之后的操作,即num–就会出现产品为负的情况。
为了再表面这种现象,我们就要使用JDK中DOC给我们推荐的方法了:wait()方法往往建议都使用在while循环里面,因此我们继续改进:
把if改成while即可:
while(num>=TOTAL) {}
这样我们再次运行,完美,没任何毛病。不管我们用多少个生产者、消费者,都没有问题了。
总结
按照官方JDK说的,虚假唤醒在wait的时候是有可能发生的,因此建议都放在while循环里,这样能够完全的避免问题。