一、什么是虚假唤醒?
多线程环境下,有多个线程执行了wait()方法,需要其他线程执行notify()或者notifyAll()方法去唤醒它们,假如多个线程都被唤醒了,但是只有其中一部分是有用的唤醒操作,其余的唤醒都是无用功;对于不应该被唤醒的线程而言,便是虚假唤醒。
比如:仓库有货了才能出库,突然仓库入库了一个货品;这时所有的线程(货车)都被唤醒,来执行出库操作;实际上只有一个线程(货车)能执行出库操作,其他线程都是虚假唤醒。
下图为JAVA8中对wait()方法虚假唤醒的介绍:
意思是:虚假唤醒是可能发生的,我们应该在while循环中使用wait()方法。
二、怎么产生虚假唤醒?
假设当前有4个线程分别为ProducerA,ProducerB,ConsumerC,ConsumerD,其中ProducerA、ProducerB线程是生产者,ConsumerC、ConsumerD线程是消费者。当ProducerA或ProducerB线程生产了一个数据后会通知消费者去消费,ConsumerC或ConsumerD消费掉该条数据后会通知生产者去生产,数据的大小为1。也就是说正常情况下,数据只会有0和1两种值,0表示生产者该生产数据了,1表示消费者该消费数据了。
public class SpuriousWakeup2 {
public static void main(String[] args) {
WareHouse wareHouse = new WareHouse();
Producer producer = new Producer(wareHouse);
Customer customer = new Customer(wareHouse);
new Thread(producer, "ProducerA").start();
new Thread(producer, "ProducerB").start();
new Thread(customer, "ConsumerC").start();
new Thread(customer, "ConsumerD").start();
}
}
// 仓库
class WareHouse {
private volatile int product = 0;
// 入库
public synchronized void purchase() {
// 库存已满,仓库最多容纳1个货品
if (product > 0) {
System.out.println(Thread.currentThread().getName() + ": " + "已满!");
try {
this.wait();
} catch (InterruptedException e) {
// ignore exception
}
}
++product;
// 该线程从while中出来的时候,已满足条件
System.out.println(Thread.currentThread().getName() + ": " + "-------------入库成功,余货:" + product);
this.notifyAll();
}
// 出库
public synchronized void outbound() {
if (product <= 0) {
System.out.println(Thread.currentThread().getName() + ": " + "库存不足,无法出库");
try {
this.wait();
} catch (InterruptedException e) {
// ignore exception
}
}
--product;
System.out.println(Thread.currentThread().getName() + ":出库成功,余货:" + product);
this.notifyAll();
}
}
// 生产者
class Producer implements Runnable {
private WareHouse wareHouse;
public Producer(WareHouse wareHouse) {
this.wareHouse = wareHouse;
}
@Override
public void run() {
for (int i = 0; i < 5; ++i) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
}
wareHouse.purchase();
}
}
}
// 消费者
class Customer implements Runnable {
private WareHouse wareHouse;
public Customer(WareHouse wareHouse) {
this.wareHouse = wareHouse;
}
@Override
public void run() {
for (int i = 0; i < 5; ++i) {
wareHouse.outbound();
}
}
}
运行结果:
我丢,看这数据就离谱,怎么还有-1、-9的?
出现这种问题的场景如是:当product为1的时候,说明线程ProducerA或者ProducerB已经生产了一条数据,其会执行notifyAll()唤醒线程ConsumerC和ConsumerD,然而被唤醒的两个Consumer线程,并没有再次去判断product的状态,便执行了–product逻辑,导致两个Comsumer同时执行了outbound()方法。也就出现上面所示的运行结果。
问题的关键点在于:程序仅使用if对product做了一次判断,我们应该使用while循环去判断。即wait()要在while循环中。
1、为什么 if会出现虚假唤醒?
- 因为if只会执行一次,执行完会接着向下执行if(){}后边的逻辑;
- 而while不会,直到条件满足才会向下执行while(){}后边的逻辑。
三、如何避免虚假唤醒
使用while循环去循环判断一个条件,而不是使用if只判断一次条件;即wait()要在while循环中。
修改后的代码:
public class SpuriousWakeup2 {
public static void main(String[] args) {
WareHouse wareHouse = new WareHouse();
Producer producer = new Producer(wareHouse);
Customer customer = new Customer(wareHouse);
new Thread(producer, "ProducerA").start();
new Thread(producer, "ProducerB").start();
new Thread(customer, "ConsumerC").start();
new Thread(customer, "ConsumerD").start();
}
}
// 仓库
class WareHouse {
private volatile int product = 0;
// 入库
public synchronized void purchase() {
// 库存已满,仓库最多容纳1个货品
while (product > 0) {
System.out.println(Thread.currentThread().getName() + ": " + "已满!");
try {
this.wait();
} catch (InterruptedException e) {
// ignore exception
}
}
++product;
// 该线程从while中出来的时候,已满足条件
System.out.println(Thread.currentThread().getName() + ": " + "-------------入库成功,余货:" + product);
this.notifyAll();
}
// 出库
public synchronized void outbound() {
while (product <= 0) {
System.out.println(Thread.currentThread().getName() + ": " + "库存不足,无法出库");
try {
this.wait();
} catch (InterruptedException e) {
// ignore exception
}
}
--product;
System.out.println(Thread.currentThread().getName() + ":出库成功,余货:" + product);
this.notifyAll();
}
}
// 生产者
class Producer implements Runnable {
private WareHouse wareHouse;
public Producer(WareHouse wareHouse) {
this.wareHouse = wareHouse;
}
@Override
public void run() {
for (int i = 0; i < 5; ++i) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
}
wareHouse.purchase();
}
}
}
// 消费者
class Customer implements Runnable {
private WareHouse wareHouse;
public Customer(WareHouse wareHouse) {
this.wareHouse = wareHouse;
}
@Override
public void run() {
for (int i = 0; i < 5; ++i) {
wareHouse.outbound();
}
}
}
运行结果: