大家好,今天来跟大家聊聊某小厂的一道面试题,什么是虚假唤醒。
生产者消费者模型引出虚假唤醒的问题
说虚假唤醒之前,我们来测试一段经典的生产者和消费者代码。
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 (<condition does not hold>)
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来解决。