多线程:什么是虚假唤醒?为什么会产生虚假唤醒?
  KVXdOywJDl3A 2023年11月02日 66 0


最近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,一旦生产者生产了产品,消费者就要去消费而生产者不得再生产。
理论上应该出现的结果:

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

``
步骤分析:

生产者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,这样线程就会被重复判断而不再会跳出判断代码块,从而不会产生虚假唤醒这种情况了。

改动后的代码:

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的区别

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

推荐阅读
  2Vtxr3XfwhHq   2024年05月17日   53   0   0 Java
  Tnh5bgG19sRf   2024年05月20日   109   0   0 Java
  8s1LUHPryisj   2024年05月17日   46   0   0 Java
  aRSRdgycpgWt   2024年05月17日   47   0   0 Java
KVXdOywJDl3A