Java并发编程的虚假唤醒以及为什么用if做wait判断数据会变乱和为什么用了Synchronized关键字还会感觉像是多个线程同时进入了该方法

一、什么是虚假唤醒?

  多线程环境下,有多个线程执行了wait()方法,需要其他线程执行notify()或者notifyAll()方法去唤醒它们,假如多个线程都被唤醒了,但是只有其中一部分是有用的唤醒操作,其余的唤醒都是无用功;对于不应该被唤醒的线程而言,便是虚假唤醒。
比如:仓库有货了才能出库,突然仓库入库了一个货品;这时所有的线程(货车)都被唤醒,来执行出库操作;实际上只有一个线程(货车)能执行出库操作,其他线程都是虚假唤醒。

  以下为jdk8的文档中,对虚假唤醒的介绍:

在这里插入图片描述

  文档中说,为了防止虚假唤醒的发生,最好在执行wait()方法的判断条件中用while
循环来判断,而不是if

二、怎么产生虚假唤醒?

  我们定义四个线程,分别是ProducersA、ProducersB、ConsumersC、ConsumersD。当ProducerA或ProducerB线程生产了一个数据后会通知消费者去消费,ConsumerC或ConsumerD消费掉该条数据后会通知生产者去生产,数据的大小为1。也就是说正常情况下,数据只会有0和1两种值,0表示生产者该生产数据了,1表示消费者该消费数据了。

  判断条件为只有 cnt 为0时,生产者才能生产,只有当cnt大于0时,消费者才能消费,否则等待。

public class SynchronizedPC {  // Synchronized版本的生产者消费者问题
    public static void main(String[] args) {

        Data data = new Data();

        new Thread(()->{ //相当于是在Runnable接口
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment(); // 相当于在run方法中调用该方法
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"ProducersA").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"ProducersB").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"ConsumersC").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"ConsumersD").start();

    }
}

  我们把处理资源的操作放在Data类中,然后用lambda表达式去实现Runnable接口,在run方法中调用对资源的操作。

/*
* 等待,业务处理,唤醒
* 1、如果资源不满足业务处理的要求,该进程就等待。
* 2、资源满足业务处理要求,就进行业务处理
* 3、处理完资源之后,唤醒其他所有进程,看其他进程是否可以远行,回到第一步
* */
class Data{  //资源类
    private static volatile int cnt = 0;

    //+1
    public synchronized void increment() throws InterruptedException {
        if (cnt != 0){  // 只能为0才能加
            this.wait();  // 等待
        }
        cnt++;
        System.out.println(Thread.currentThread().getName() + "=>" + cnt);  //输出调用此方法的线程名字
        //通知其他线程我+1完毕
        this.notifyAll();
    }

    //-1
    public synchronized void decrement() throws InterruptedException {
        if (cnt == 0){  // 只能不为0才能减
            this.wait();  // 等待
        }
        cnt--;
        System.out.println(Thread.currentThread().getName() + "=>" + cnt);
        //通知其他线程我-1完毕
        this.notifyAll();
    }
}

  线程如果没有被wait(),并且执行完操作之后,会调用**notifyAll()**方法,唤醒其他所有线程。

  我们先用if做**wait()**的条件判断,部分结果如下:

A=>1
C=>0
C=>-1
C=>-2
C=>-3
C=>-4
C=>-5
C=>-6
D=>-7
D=>-8
D=>-9
D=>-10
D=>-11
D=>-12
D=>-13
B=>-12
A=>-11
B=>-10
A=>-9
B=>-8
A=>-7
B=>-6

  我一开始的理解是,方法不是用了Synchronized修饰吗,不是同时只能有一个线程操作该方法吗,并且只有cnt为0时才能进行生产,那么cnt不应该超过0才对啊?为什么呢?

三、解释

  一开始别人和我说因为产生了虚假唤醒,用虚假唤醒来解释这一行为,我还是很不理解,就算你唤醒几个生产线程程,但是同时只能有一个进入该方法,而且还有判断条件,线程怎么能同时操作方法进行增加呢?

  这个方法确实只能同时有一个线程去访问,但是不一定每个访问该方法的线程都会去执行这个if判断条件,这是一个重点。另外一个重点是,我忽略了休眠方法,即wait()。线程被休眠之后,下次被唤醒的话,是直接执行wait()方法之后的代码,即不会再去判断一次cnt的数量,而是直接对cnt进行改变。因为if只能判断一次,只能再第一次休眠的时候判断,重新被唤醒之后不会再执行**wait()**方法,而是执行后面的方法。

  这就是为什么这个问题是由虚假唤醒引起的了。

  我们把if改成while看看结果:

ProducersA=>1
ConsumersC=>0
ProducersB=>1
ConsumersC=>0
ProducersA=>1
ConsumersC=>0
ProducersB=>1
ConsumersC=>0
ProducersA=>1
ConsumersC=>0
ProducersB=>1
ConsumersD=>0
ProducersA=>1
ConsumersC=>0
ProducersB=>1
ConsumersD=>0
ProducersA=>1
ConsumersC=>0
ProducersB=>1
ConsumersD=>0
ProducersA=>1
ConsumersC=>0
ProducersB=>1
ConsumersD=>0
ProducersA=>1
ConsumersC=>0
ProducersB=>1
ConsumersD=>0
ProducersA=>1
ConsumersC=>0
ProducersB=>1
ConsumersD=>0
ProducersA=>1
ConsumersD=>0
ProducersB=>1
ConsumersD=>0
ProducersA=>1
ConsumersD=>0
ProducersB=>1
ConsumersD=>0

  很显然,这次的结果是符合我们的预期的。因为while会不断的对cnt进行判断,第一次被休眠之后,下一次被唤醒还会去判断cnt是否满足条件,不满足的话就继续休眠。意思就是说,if在判断一次之后,就退出if条件判断语句了。而while是一直在进行判断,直到满足条件(或者不符合条件)之后,才能退出循环,才能执行后面的语句。

  博文参考自:https://blog.csdn.net/Saintmm/article/details/121092830

posted @   爱吃雪糕的小布丁  阅读(42)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)
点击右上角即可分享
微信分享提示