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


    最近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();
        }
    }```
    
    ```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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66

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

    生产者A添加产品,剩余1件产品
    消费者A使用产品,剩余0件产品
    生产者A添加产品,剩余1件产品
    消费者A使用产品,剩余0件产品
    生产者B添加产品,剩余1件产品
    消费者A使用产品,剩余0件产品
    生产者A添加产品,剩余1件产品
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    程序实际运行结果为:

    生产者A添加产品,剩余1件产品
    消费者A使用产品,剩余0件产品
    生产者B添加产品,剩余1件产品
    生产者A添加产品,剩余2件产品
    生产者B添加产品,剩余3件产品
    消费者A使用产品,剩余2件产品
    消费者A使用产品,剩余1件产品
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

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

    为了让程序执行步骤更好理解,我在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方法");
            ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    执行结果如下:

    生产者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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    ``
    步骤分析:

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

    生产者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件产品
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    为什么会产生虚假唤醒?
    从上面的例子可以看出,同步失败的主要原因有以下几个点:

    生产者唤醒了所有处于阻塞队列中的线程,我们希望的是生产者A唤醒的应该是两个消费者,而不是唤醒了生产者B
    我们都知道,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();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    执行结果如下:

    生产者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

  • 相关阅读:
    2019年初Java开发面试经验(南京)
    利用jar包构建Docker 镜像
    【Go语言实战】(26) 分布式搜索引擎
    SpringBoot : ch05 整合Mybatis
    c++ Mixin实现的一种方法
    PostgreSQL 16数据库的yum、编译、docker三种方式安装——筑梦之路
    TinyRenderer学习笔记--从零构建软件渲染器
    Git常用指令-1
    干货分享:有什么软件可以让照片动起来?
    「前端+鸿蒙」核心技术HTML5+CSS3(十)
  • 原文地址:https://blog.csdn.net/qq_34629352/article/details/133966239