前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >某小厂面试题:什么是虚假唤醒?

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

作者头像
三友的java日记
发布2022-07-27 13:55:51
3700
发布2022-07-27 13:55:51
举报
文章被收录于专栏:原创干货

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

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

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

代码语言:javascript
复制
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的方法的注释已经告诉我们了。

我把这段注释截出来

代码语言:javascript
复制
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来解决。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-04-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 三友的java日记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档