线程虚假唤醒问题描述
在JDK API文档中,关于Object类的wait()方法有这样一句话描述"线程也可以唤醒,而不会被通知,中断或超时,即所谓的虚假唤醒 。 虽然这在实践中很少会发生,但应用程序必须通过测试应该使线程被唤醒的条件来防范,并且如果条件不满足则继续等待",如下图所示:
在多线程的情况下,当多个线程执行了wait()方法后,需要其它线程执行notify()或者notifyAll()方法去唤醒,假如被阻塞的多个线程都被唤醒,但实际情况是被唤醒的线程中有一部分线程是不应该被唤醒的,那么对于这些不应该被唤醒的线程而言就是虚假唤醒。
问题复现
生产者与消费者问题
假设当前有4个线程分别为A,B,C,D,其中A,B线程是生产者,C,D线程是消费者,当A和B线程生产了一个数据后就去通知消费者去消费,C和D消费掉这一个数据后就通知生产者去生产,数据的大小为1。也就是说正常情况下,数据只会有0和1两种状态,0表示生产者该生产数据了,1表示消费者该消费数据了。
package producer_consumer;
public class PVTest {
public static void main(String[] args) {
Data data = new Data();
//生产者线程A
new Thread(() -> {
for (int i = 0;i < 5;i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
//生产者线程B
new Thread(() -> {
for (int i = 0;i < 5;i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
//消费者线程C
new Thread(() -> {
for (int i = 0;i < 5;i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
//消费者线程D
new Thread(() -> {
for (int i = 0;i < 5;i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}
}
//数据类
class Data {
//表示数据个数
private int number = 0;
public synchronized void increment() throws InterruptedException {
//关键点,这里应该使用while循环
if (number != 0) {
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName()+"生产了数据:"+number);
this.notifyAll();
}
public synchronized void decrement() throws InterruptedException {
//关键点,这里应该使用while循环
if (number == 0) {
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"消费了数据:"+number);
this.notifyAll();
}
}
程序结果
结果分析
可以看到上述结果出现了data为2的情况,不符合之前的预期,出现问题的场景是这样的:当data为1的时候,线程A和B先后获取锁去生产数据的时候会被阻塞住,然后消费者C或者D消费掉数据后去notifyAll()唤醒了线程A和B,被唤醒的A和B没有再次去判断data状态,就去执行后续增加数据的逻辑了,导致两个生产者都执行了increment(),最终data出现了2这种情况。也就是说线程A和B有一个是不应该被唤醒的却被唤醒了,出现这个问题的关键点在于程序中使用到了if判断,只判断了一次data的状态,应该使用while循环去判断
虚假唤醒问题解决
正如JDK API文档中所说在写程序时候应该用while去替代if,上述生产者和消费者代码中,将Data类中的increment()和decrement()方法中的if换为while即可避免线程虚假唤醒的问题。