查看原文
其他

某小厂面试题:什么是虚假唤醒?

三友 三友的java日记 2022-10-31

大家好,今天来跟大家聊聊某小厂的一道面试题,什么是虚假唤醒。


生产者消费者模型引出虚假唤醒的问题


说虚假唤醒之前,我们来测试一段经典的生产者和消费者代码。

public class SpuriousWakeupDemo {
public static void main(String[] args) throws Exception{ Producer producer = new Producer();
new Thread(() -> { for (int i = 0; i < 10; i++) { try { producer.increment(); } catch (Exception exception) { exception.printStackTrace(); } } },"生产者线程A").start();
new Thread(() -> { for (int i = 0; i < 5; i++) { try { producer.decrement(); } catch (Exception exception) { exception.printStackTrace(); } } },"消费者线程B").start();
new Thread(() -> { for (int i = 0; i < 5; i++) { try { producer.decrement(); } catch (Exception exception) { exception.printStackTrace(); } } },"消费者线程C").start();
}

static class Producer {
private int count = 0;
public synchronized void increment() throws Exception { if (count > 0) { wait(); }
count++;
System.out.println("【" + Thread.currentThread().getName() + "】生产后数量为" + count);
notifyAll(); }
public synchronized void decrement() throws Exception { if (count <= 0) { wait(); }
count--;
System.out.println("【" + Thread.currentThread().getName() + "】消费后数量为" + count);
notifyAll(); } }
}


这段代码很简单,Producer类提供了两个方法, increment方法先判断count是否大于0,是的话就会调用wait方法等待,小于等于0或者被唤醒之后,将count加1;decrement方法先判断count是不是小于等于0,是的话就会等待,如果不小于0或者被唤醒之后将count减1。


然后开了三个线程,线程A循环调用10次increment方法,线程B和线程C循环调用5次decrement方法。按照代码的写法,方法都加锁了,增加count或者减少count之前都进行了判断,应该不会出现线程安全的问题。但是真的不会有问题,下面放上这段代码的测试截图。



通过上面的运行结果,我们可以看见,竟然出现消费了count之后,出现了负数情况,这是怎么回事,会什么会出现线程不安全的情况,每次减少之前不都是先进行count<=0的判断么,小于0会阻塞的,直到count>0才会被唤醒,但是为什么还是出现负数?


接下来我们来分析一下这段代码为什么会出现负数的问题。


假设某一时刻,count 为 0 ,B、C两个消费者线程按顺序(因为加锁的缘故)调用decrement都发现count为0,就都会调用wait方式进行释放锁进行等待,然后线程A也调用increment,判断是0,不满足调用wait条件,然后将count加成1之后,调用notifyAll方法同时唤醒B、C线程,A执行完代码,释放了锁;B、C被唤醒之后,假设B抢到锁,C没抢到,C继续阻塞,B从wait方法那继续往下走,将count减1,此时count变为0,B执行完释放了锁之后C这时抢到了锁,也从wait方法那继续执行代码,然后也将count减1,这下出现问题了,线程B减完之后就是0了,线程C又将count=0减1,那不就变成-1了,所以这就产生的负数的情况。


什么虚假唤醒?


其实产生这种负数的情况就是虚假唤醒导致的。那什么虚假唤醒呢,虚假唤醒就是由于把所有线程都唤醒了,但是只有其中一部分是有用的唤醒操作,其余的唤醒都是无用功,对于不应该被唤醒的线程而言,便是虚假唤醒。


对于上面这个例子来说,由于只应该唤醒一个线程,因为count加1之后只能满足被1个线程消费的条件,但是两个都唤醒了,才会出现两个线程都去减1的情况,从而出现负数的现象。


如何解决虚假唤醒?


那怎么来避免出现这种虚假唤醒的情况呢,其实wait的方法的注释已经告诉我们了。


我把这段注释截出来

As in the one argument version, interrupts and spurious wakeups arepossible, and this method should always be used in a loop:<pre> synchronized (obj) { while (&lt;condition does not hold&gt;)  obj.wait(); // Perform action appropriate to condition }</pre>

这段注释主要是告诉我们,可能会出现虚假唤醒的现象,可以用过while条件来代替if条件来解决虚假唤醒的问题。在while中调用wait方法,而不是在if中


那么为什么while可以解决虚假唤醒?就拿上面的例子来说,当C获取到锁,执行代码,但是由于是while循环,再一次判断count是不是小于等于0,发现此时count是0,while条件满足,则继续调用wait方法进入等待,而不是执行count--,就避免了出现负数的情况。


下面是我将if改成while之后,代码运行的结果。



运行结果再也没有出现负数的现象,也就解决了虚假唤醒的问题。


总结


通过本篇的文章,相信大家了解什么是虚假唤醒,面试的时候也能回答到了,其实很简单,就是一个线程在唤醒等待的线程之后,有一部分是可以满足条件的,另一部分是不满足条件的,这部分不满足条件的被唤醒的线程就属于虚假唤醒,解决方法就是通过while来循环判断是不是满足条件,这样就不满足条件的线程就会再次等待。其实在这种类似生产者消费者的模型下进行if进行判断的时候,需要判断是不是可能出现虚假唤醒,是的话就需要用while来解决。


如果觉得这篇文章对你有所帮助,还请帮忙点赞、在看、转发一下,码字不易,非常感谢!


欢迎点击关注公众号,利用碎片化时间学习,每天进步一点点。

往期热门文章推荐


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存