什么是虚假唤醒?为什么会产生虚假唤醒?

最近B站学习狂神的JUC并发编程时,听到了虚假唤醒这个词,虽然狂神进行了代码的演示,但我还是不太理解为什么使用if判断包装wait方法会出现虚假唤醒,查找了网上很多大佬的博客终于理解了,这里分享一下虚假唤醒产生的原因。

什么是虚假唤醒?

当一定的条件触发时会唤醒很多在阻塞态的线程,但只有部分的线程唤醒是有用的,其余线程的唤醒是多余的。
比如说卖货,如果本来没有货物,突然进了一件货物,这时所有的顾客都被通知了,但是只能一个人买,所以其他人都是无用的通知。

虚假唤醒演示

public class test {
    public static void main(String[] args) {
        Product product = new Product();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    product.push();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "生产者A").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    product.pop();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "消费者A").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    product.push();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "生产者B").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    product.pop();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "消费者B").start();
    }
}

class Product {
    private int product = 0;

    public synchronized void push() throws InterruptedException {
        // System.out.println(Thread.currentThread().getName() + "进入push方法");
        if (product > 0) {
            this.wait();
        }
        product++;
        System.out.println(Thread.currentThread().getName() + "添加产品,剩余" + product + "件产品");
        this.notifyAll();
    }

    public synchronized void pop() throws InterruptedException {
        // System.out.println(Thread.currentThread().getName() + "进入pop方法");
        if (product == 0) {
            this.wait();
        }
        product--;
        System.out.println(Thread.currentThread().getName() + "使用产品,剩余" + product + "件产品");
        this.notifyAll();
    }
}

程序中定义了两个生产者和两个消费者,产品缓冲区的大小为1,一旦生产者生产了产品,消费者就要去消费而生产者不得再生产。
理论上应该出现的结果:

生产者A添加产品,剩余1件产品
消费者A使用产品,剩余0件产品
生产者A添加产品,剩余1件产品
消费者A使用产品,剩余0件产品
生产者B添加产品,剩余1件产品
消费者A使用产品,剩余0件产品
生产者A添加产品,剩余1件产品
...

程序实际运行结果为:

生产者A添加产品,剩余1件产品
消费者A使用产品,剩余0件产品
生产者B添加产品,剩余1件产品
生产者A添加产品,剩余2件产品
生产者B添加产品,剩余3件产品
消费者A使用产品,剩余2件产品
消费者A使用产品,剩余1件产品
...

可以看到程序并没有实现同步的需求。实际上出现的结果可能远不止如此,那为什么会出现这种情况呢?

为了让程序执行步骤更好理解,我在push和pop方法前加入输出语句:

public synchronized void push() throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "进入push方法");
        ...
}

public synchronized void pop() throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "进入pop方法");
        ...
}

执行结果如下:

生产者A进入push方法
生产者A添加产品,剩余1件产品
生产者A进入push方法
消费者A进入pop方法
消费者A使用产品,剩余0件产品
消费者A进入pop方法
生产者A添加产品,剩余1件产品
生产者A进入push方法
生产者B进入push方法
消费者A使用产品,剩余0件产品
消费者A进入pop方法
生产者B添加产品,剩余1件产品
生产者B进入push方法
生产者A添加产品,剩余2件产品
生产者A进入push方法
生产者B添加产品,剩余3件产品
...

步骤分析:

  1. 生产者A先进入push方法,此时没有产品,条件判断不成立,生产产品,唤醒其他线程
if (product > 0){
    this.wait();
}
生产者A进入push方法
生产者A添加产品,剩余1件产品
  1. 生产者A继续进入push方法,但是此时已有一个产品,条件满足,进入阻塞队列并释放锁
生产者A进入push方法
  1. 消费者A进入pop方法,此时已有产品,条件不满足,使用一个产品并唤醒其他线程。
if (product == 0) {
    this.wait();
}
消费者A进入pop方法
消费者A使用产品,剩余0件产品
  1. 消费者A的CPU时间片未结束,继续进入pop方法,但此时已没有产品了,进入阻塞队列并释放锁
消费者A进入pop方法
  1. 由于步骤3已经唤醒了生产者A线程(注意生产者A停留在if代码块中),此时生产者A直接跳出 if 代码块并添加产品并唤醒其他线程
生产者A添加产品,剩余1件产品
  1. 生产者A时间片未结束,继续进入push方法,此时有产品,进入阻塞队列
生产者A进入push方法
  1. 生产者B进入push方法,此时有产品,进入阻塞队列
生产者B进入push方法
  1. 在步骤5中唤醒了阻塞队列中的消费者A线程,此时消费者A跳出 if 代码块消费产品并唤醒了生产者A线程、生产者B线程,由于时间片未结束,消费者A继续进入pop方法,但此时已经没有产品了,进入阻塞队列
消费者A使用产品,剩余0件产品
消费者A进入pop方法
  1. 经过这么久,终于要到发生同步错误的地方了!!!注意步骤8中消费者A唤醒了位于阻塞队列中的生产者A线程和生产者B线程,而这两个线程此时停留在if代码块中。
    首先 CPU时间片给到了生产者B,生产者B生产了一个产品,但时间片未结束,继续进入push方法,此时已有产品,因此生产者B停留在this.wait()处
if (product > 0) {
    this.wait();
}
生产者B添加产品,剩余1件产品
生产者B进入push方法
  1. 此时CPU时间片给到了生产者A,生产者A跳出if判断条件,添加一个产品(此时产品变为两个)并唤醒其他线程(生产者B线程又被唤醒了),同样CPU时间片未结束会产生和步骤9生产者线程B同样的操作
生产者A添加产品,剩余2件产品
生产者A进入push方法
  1. 在步骤10中生产者B线程又被唤醒,此时CPU时间片又给到生产者B,生产者跳出 if 代码块并生产一个产品(此时产品变为3个)…
生产者B添加产品,剩余3件产品

如此一来,两个生产者就有可能一直往复生产下去,产品数量可能变得很大。同时,若两个消费者一直交替消费产品,那产品数量可能就会出现负数的情况。如下面运行结果:

...
消费者B进入pop方法
消费者B使用产品,剩余0件产品
消费者B进入pop方法
消费者A使用产品,剩余-1件产品
消费者A进入pop方法
消费者A使用产品,剩余-2件产品
消费者B使用产品,剩余-3件产品
生产者B添加产品,剩余-2件产品
生产者B进入push方法
生产者B添加产品,剩余-1件产品
生产者A添加产品,剩余0件产品
...

为什么会产生虚假唤醒?

从上面的例子可以看出,同步失败的主要原因有以下几个点:

  1. 生产者唤醒了所有处于阻塞队列中的线程,我们希望的是生产者A唤醒的应该是两个消费者,而不是唤醒了生产者B
  2. 我们都知道,wait方法的作用是将线程停止执行并送入到阻塞队列中,但是wait方法还有一个操作就是释放锁。因此当生产者A执行wait方法时,该线程就会把它持有的对象锁释放,这样生产者B就可以拿到锁进入synchronized修饰的push方法中,即使它被卡在if判断,但被唤醒后它就会又添加一个产品了。

如何解决虚假唤醒?

从上面分析可以知道导致虚假唤醒的原因主要就是一个线程直接在if代码块中被唤醒了,这时它已经跳过了if判断。我们只需要将if判断改为while,这样线程就会被重复判断而不再会跳出判断代码块,从而不会产生虚假唤醒这种情况了。

改动后的代码:

public synchronized void push() throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "进入push方法");
        while (product > 0) {
            this.wait();
        }
        product++;
        System.out.println(Thread.currentThread().getName() + "添加产品,剩余" + product + "件产品");
        this.notifyAll();
    }

public synchronized void pop() throws InterruptedException {
	    System.out.println(Thread.currentThread().getName() + "进入pop方法");
	    while (product == 0) {
	        this.wait();
	    }
	    product--;
	    System.out.println(Thread.currentThread().getName() + "使用产品,剩余" + product + "件产品");
	    this.notifyAll();
    }

执行结果如下:

生产者A进入push方法
生产者A添加产品,剩余1件产品
生产者A进入push方法
消费者A进入pop方法
消费者A使用产品,剩余0件产品
消费者A进入pop方法
生产者A添加产品,剩余1件产品
生产者A进入push方法
消费者A使用产品,剩余0件产品
消费者A进入pop方法
生产者A添加产品,剩余1件产品
生产者A进入push方法
消费者A使用产品,剩余0件产品
...

可以看出,无论CPU时间片给到哪个线程都不会再发生虚假唤醒了

参考:

  1. 什么是Java虚假唤醒及如何避免虚假唤醒?《多线程学习之十四》
  2. Java中Synchronized的用法(简单介绍)
  3. java并发编程:wait()和sleep的区别
  • 61
    点赞
  • 104
    收藏
    觉得还不错? 一键收藏
  • 17
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值