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

pivoteic
发布于 2022-6-13 17:50
浏览
0收藏

 

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

 

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

 

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

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 are
possible, 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来解决。

 

文章转自公众号:三友的java日记

标签
已于2022-6-13 17:50:13修改
收藏
回复
举报
回复
    相关推荐