多线程:什么是虚假唤醒?为什么会产生虚假唤醒?

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

什么是虚假唤醒?

当一定的条件触发时会唤醒很多在阻塞态的线程,但只有部分的线程唤醒是有用的,其余线程的唤醒是多余的。

比如说卖货,如果本来没有货物,突然进了一件货物,这时所有的顾客都被通知了,但是只能一个人买,所以其他人都是无用的通知。

虚假唤醒演示

java 复制代码
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();
    }
}```

```java
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方法前加入输出语句:

java 复制代码
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件产品

``

步骤分析:

生产者A先进入push方法,此时没有产品,条件判断不成立,生产产品,唤醒其他线程
if (product > 0){
    this.wait();
}
生产者A进入push方法
生产者A添加产品,剩余1件产品

生产者A继续进入push方法,但是此时已有一个产品,条件满足,进入阻塞队列并释放锁

生产者A进入push方法

消费者A进入pop方法,此时已有产品,条件不满足,使用一个产品并唤醒其他线程。

if (product == 0) {

this.wait();

}

消费者A进入pop方法

消费者A使用产品,剩余0件产品

消费者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

2

经过这么久,终于要到发生同步错误的地方了!!!注意步骤8中消费者A唤醒了位于阻塞队列中的生产者A线程和生产者B线程,而这两个线程此时停留在if代码块中。

首先 CPU时间片给到了生产者B,生产者B生产了一个产品,但时间片未结束,继续进入push方法,此时已有产品,因此生产者B停留在this.wait()处

if (product > 0) {

this.wait();

}

生产者B添加产品,剩余1件产品

生产者B进入push方法

此时CPU时间片给到了生产者A,生产者A跳出if判断条件,添加一个产品(此时产品变为两个)并唤醒其他线程(生产者B线程又被唤醒了),同样CPU时间片未结束会产生和步骤9生产者线程B同样的操作

生产者A添加产品,剩余2件产品

生产者A进入push方法

在步骤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件产品

为什么会产生虚假唤醒?

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

生产者唤醒了所有处于阻塞队列中的线程,我们希望的是生产者A唤醒的应该是两个消费者,而不是唤醒了生产者B

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

如何解决虚假唤醒?

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

改动后的代码:

java 复制代码
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时间片给到哪个线程都不会再发生虚假唤醒了

参考:

什么是Java虚假唤醒及如何避免虚假唤醒?《多线程学习之十四》

Java中Synchronized的用法(简单介绍)

java并发编程:wait()和sleep的区别


版权声明:本文为CSDN博主「橙不甜橘不酸」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/weixin_45668482/article/details/117373700

相关推荐
亚图跨际1 分钟前
Python和R荧光分光光度法
开发语言·python·r语言·荧光分光光度法
Rverdoser10 分钟前
RabbitMQ的基本概念和入门
开发语言·后端·ruby
dj244294570713 分钟前
JAVA中的Lamda表达式
java·开发语言
工业3D_大熊27 分钟前
3D可视化引擎HOOPS Luminate场景图详解:形状的创建、销毁与管理
java·c++·3d·docker·c#·制造·数据可视化
szc176730 分钟前
docker 相关命令
java·docker·jenkins
程序媛-徐师姐40 分钟前
Java 基于SpringBoot+vue框架的老年医疗保健网站
java·vue.js·spring boot·老年医疗保健·老年 医疗保健
yngsqq41 分钟前
c#使用高版本8.0步骤
java·前端·c#
流星白龙43 分钟前
【C++习题】10.反转字符串中的单词 lll
开发语言·c++
尘浮生1 小时前
Java项目实战II基于微信小程序的校运会管理系统(开发文档+数据库+源码)
java·开发语言·数据库·微信小程序·小程序·maven·intellij-idea
MessiGo1 小时前
Python 爬虫 (1)基础 | 基础操作
开发语言·python