虚假唤醒的问题

一、传统的Synchronized版本的生产者消费者实现

说起虚假唤醒问题就要提起我们的生产者和消费者,我们先看两段代码

1.1、当只有两个线程时,代码运行正常

代码如下

package com.JUC.procon;

/**
 * @Author Feng
 * @Date 2021/10/19 10:04
 * @Version 1.0
 * @Description
 *
 * 线程之间的通信问题:生产者和消费者问题 !   等待唤醒,通知唤醒
 * 线程交替执行:A线程和B线程,操作同一个变量 num = 0
 * A:num+1
 * B:num-1
 */
public class A {
    public static void main(String[] args) {
        Data data = new Data();
        //A线程加1
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();
        //B线程减一
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"B").start();
    }
}


//生产者消费者的三部曲
// 1.判断等待 2.业务代码 3.通知唤醒
class Data{//数字,资源类

    private int num =0;

    // +1的方法
    public synchronized void increment() throws InterruptedException {
        if (num!=0){
            this.wait();//不等于0的时候等待
        }
        //等于0的时候加1
        num++;
        System.out.println(Thread.currentThread().getName()+"==>"+num);
        this.notifyAll();//加完后唤醒
    }

    // -1的方法
    public synchronized void decrement() throws InterruptedException {
        if (num==0){
            this.wait();//不等于0的时候等待
        }
        //等于1的时候减1
        num--;
        System.out.println(Thread.currentThread().getName()+"==>"+num);
        this.notifyAll();//减完后唤醒其他线程
    }

}

结果如下
在这里插入图片描述

  • 此时代码的运行是逻辑上是没有问题的,生产者生产一个,消费者就消费一个;当没有库存而消费者线程被唤醒时,则会wait()进入等待被挂起。但是这个代码是有严重设计缺陷的,在线程体进行条件判断时应该使用while而非if。为什么呢?请看后面的讲解。这个之所以不出问题是因为生产者和消费者线程都只有一个。此时如果消费者线程被阻塞,则它只有等待生产者线程调用notifyAll()来唤醒它,而在唤醒它之前,生产者已经完成了生产操作,从而使得没有出现异常。
    但是当线程多了之后,就会出现问题

1.2、当有多个线程时,代码执行结果就会有错误

我们为这个资源类新增两个线程C和线程D时。

package com.JUC.procon;

/**
 * @Author Feng
 * @Date 2021/10/19 10:04
 * @Version 1.0
 * @Description :传统的synchronized版本
 * 线程之间的通信问题:生产者和消费者问题 !   等待唤醒,通知唤醒
 * 线程交替执行:A线程和B线程,操作同一个变量 num = 0
 * A:num+1
 * B:num-1
 */
public class A {
    public static void main(String[] args) {
        Data data = new Data();
        //A线程加1
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();
        //B线程减一
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "C").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "D").start();
    }
}


//生产者消费者的三部曲
// 1.判断等待 2.业务代码 3.通知唤醒
class Data {//数字,资源类

    private int num = 0;

    // +1的方法
    public synchronized void increment() throws InterruptedException {
        if (num != 0) {
            this.wait();//不等于0的时候等待
        }
        //等于0的时候加1
        num++;
        System.out.println(Thread.currentThread().getName() + "==>" + num);
        this.notifyAll();//加完后唤醒
    }

    // -1的方法
    public synchronized void decrement() throws InterruptedException {
        if (num == 0) {
            this.wait();//不等于0的时候等待
        }
        //等于1的时候减1
        num--;
        System.out.println(Thread.currentThread().getName() + "==>" + num);
        this.notifyAll();//减完后唤醒其他线程
    }

}

运行结果

在这里插入图片描述
我们发现出现了2,3 ,按照逻辑,应该是只有0和1 ,但是出现了2和3,就证明我们出现了问题。有线程多生产了。这是为什么呢?

其实这就是 虚假唤醒问题

二、虚假唤醒

首先需要说明的是,虚假唤醒不是Java语言特有的问题,而是多线程通信特有的问题,在java中就体现在 sychronized-wait-notify上,最典型的应用场景就是 生产者-消费者模式。

定义
首先我们观看官方的文字定义

A spurious wakeup happens when a thread wakes up from waiting on a condition variable that’s been signaled, only to discover that the condition it was waiting for isn’t satisfied. It’s called spurious because the thread has seemingly been awakened for no reason. But spurious wakeups don’t happen for no reason: they usually happen because, in between the time when the condition variable was signaled and when the waiting thread finally ran, another thread ran and changed the condition. There was a race condition between the threads, with the typical result that sometimes, the thread waking up on the condition variable runs first, winning the race, and sometimes it runs second, losing the race. ----from Wikipedia

当线程从等待状态中被唤醒时,只是发现未满足其正在等待的条件时,就会发生虚假唤醒。 之所以称其为虚假的,是因为该线程似乎无缘无故被唤醒。 虚假唤醒不会无缘无故发生,通常是因为在发起唤醒号和等待线程最终运行之间的临界时间内,线程不再满足竞态条件。

我们再在JDK文档中的Thread类的wait方法中发现虚假唤醒的有关说明
在这里插入图片描述
我们发现在这个官方的代码中,它用的是while来进行条件判断而不是用if,为什么呢?

在说明两者的区别之前,我们需要明白,当一个线程调用同步对象的wait方法后,当前线程会:

  • 释放CPU
  • 释放对象锁
  • 只有等待该同步对象调用notify/notifyAll该线程才会被唤醒,唤醒后继续从wait处的下一行代码开始执行

这里最关键的就是:线程被唤醒后wait之后的代码执行的,如上面我的代码从num++;开始执行
在这里插入图片描述
这意味着:

如果使用if,条件判断只进行一次,下次被唤醒的时候已经绕过了条件判断,从wait后的语句开始顺序执行;

如果使用while,wait后的语句在循环体内,虽然绕过了上一次的条件判断,但终究会进入下一轮条件判断。

例子

  1. 消费者D消费了一个产品,此时库存为0。当前线程调用notifyAll,从wait中唤醒线程
  2. 生产者A被唤醒,拿到CPU和对象锁,生产一个产品,此时库存为1。当前线程调用notifyAll,从wait中唤醒线程
  3. 问题来了:由于notifyAll只能随机唤醒wait中的线程,它将生产者C唤醒了。由于之前生产者Cwait阻塞时已经执行过了if,此处直接向下执行业务代码num++操作,没有在进行产品的条件判断。将库存从1变为了2.这样就出现了错误的2,然后还是这样出现了3

后续的所有问题就是这样导致的。这也就是为什么我们的条件判断应该用while的原因。总结起来,导致错误的原因有:

  • notify/notifyAll无法指定唤醒线程,只能从wait的阻塞队列中随机唤醒
  • 被唤醒的线程从wait语句下一行开始执行,导致绕过了if的条件判断

这样的一个过程在wait-notify机制下是无法避免的,因为notify是随机唤醒的。因为wait会下释放锁,线程唤醒之后是从wait的位置获得锁继续执行,而这里是唤醒全部的notifyAll(),所以两个生产者都会被唤醒,假如本应该让旧生产者获取锁生产的,但是被新的生产者获得锁就会重复生产,

再者这里是 if判断 解除阻塞状态时,唤醒后线程会从wait之后的代码开始运行,但是不会重新判断if条件,直接继续运行if代码块之后的代码,他只会判断一次不会再进行判断了(因为wait之前已经进行了判断),它只会继续执行接下来的代码,

而使用while时,在解除阻塞时,会一直进行判断,需要线程满足条件时,才会执行接下来的代码

举例:**

本人的理解就是:就是在等待队列中的线程,被随机cpu调度唤醒了,但是被条件限制了,本来应该被唤醒然后执行的,但是被条件约束,这样就造成了虚假唤醒,通俗讲:就是唤醒了但是没有完全唤醒,哈哈哈!

解决方式:将 if 改为用 while 来判断条件

package com.JUC.procon;

/**
 * @Author Feng
 * @Date 2021/10/19 10:04
 * @Version 1.0
 * @Description :传统的synchronized版本
 * 线程之间的通信问题:生产者和消费者问题 !   等待唤醒,通知唤醒
 * 线程交替执行:A线程和B线程,操作同一个变量 num = 0
 * A:num+1
 * B:num-1
 */
public class A {
    public static void main(String[] args) {
        Data data = new Data();
        //A线程加1
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();
        //B线程减一
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "C").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "D").start();
    }
}


//生产者消费者的三部曲
// 1.判断等待 2.业务代码 3.通知唤醒
class Data {//数字,资源类

    private int num = 0;

    // +1的方法
    public synchronized void increment() throws InterruptedException {
        while (num != 0) {
            this.wait();//不等于0的时候等待
        }
        //等于0的时候加1
        num++;
        System.out.println(Thread.currentThread().getName() + "==>" + num);
        this.notifyAll();//加完后唤醒
    }

    // -1的方法
    public synchronized void decrement() throws InterruptedException {
        while (num == 0) {
            this.wait();//不等于0的时候等待
        }
        //等于1的时候减1
        num--;
        System.out.println(Thread.currentThread().getName() + "==>" + num);
        this.notifyAll();//减完后唤醒其他线程
    }

}

结果又正常了
在这里插入图片描述

其实还有一个更有意思的理解,我们发现当用while条件限制后,假如notifyAll()把生产者3给唤醒了,系统需要给其资源,但是又被条件限制了,然后又进入了阻塞。这个让其醒了又睡了,有意思。这个可能才是真正的虚假唤醒。

当线程从等待状态中被唤醒时,只是发现未满足其正在等待的条件时,就会发生虚假唤醒。 之所以称其为虚假的,是因为该线程似乎无缘无故被唤醒。 虚假唤醒不会无缘无故发生,通常是因为在发起唤醒号和等待线程最终运行之间的临界时间内,线程不再满足竞态条件。

好了,这就是虚假唤醒问题,我们需要注意用while来循环进行条件判断,这样就限制了虚假唤醒的出现。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 在Java中,虚假唤醒(spurious wakeup)是指一个等待线程在没有收到明确的通知或信号的情况下从等待状态中返回。这可能是由于系统噪声、编译器优化或其他原因导致的。虽然这种情况很少发生,但是在多线程编程中需要注意处理虚假唤醒的情况,以确保程序的正确性和可靠性。为了避免虚假唤醒,可以使用循环等待模式来检查等待条件是否满足,而不是仅仅依赖于wait()方法的返回。例如,在使用wait()方法等待某个条件时,可以使用while循环来检查等待条件是否满足,而不是使用if语句。 ### 回答2: Java中的虚假唤醒是指在多线程环境下,一个或多个线程在等待条件达成时被错误地唤醒。通常情况下,当一个线程调用对象的wait()方法后,它会进入等待状态,直到其他线程调用对象的notify()或notifyAll()方法来唤醒它。 然而,在某些情况下,虽然没有线程调用notify()或notifyAll()方法,等待线程仍然被唤醒。这种被错误地唤醒的情况就被称为虚假唤醒虚假唤醒可能会导致程序逻辑出现问题,因为等待线程在条件未满足的情况下提前被唤醒,从而导致程序执行不正常或结果不准确。 虚假唤醒可能发生在以下情况下: 1. 当多个线程等待同一个条件时,某个线程被唤醒,但条件仍未满足。此时其他线程可能被错误地唤醒。 2. 当等待线程在等待过程中被中断,然后重新竞争到锁。虽然它们之前已经收到了中断信号,但由于虚假唤醒的存在,它们会被错误地唤醒。 为了防止虚假唤醒的发生,通常在Java中使用循环来检查条件,而不仅仅依赖于等待和唤醒操作。例如,可以使用while循环来检查条件是否满足,如果条件不满足,则继续等待。这样可以避免虚假唤醒带来的问题,并保证程序的正确性和可靠性。 ### 回答3: 在Java中,虚假唤醒是指当一个线程在等待其他线程发送通知时,被唤醒但却没有相应的通知的情况。虚假唤醒可能会发生在使用Object类的wait()、notify()和notifyAll()方法时。 虚假唤醒是由于多线程环境下的竞态条件引起的。假设有多个线程等待一个共享资源,可能会出现这样的情况:一个线程被唤醒,但是在它检查共享资源之前,另一个线程已经修改了该资源的状态,导致出现错误的结果。 为了防止虚假唤醒产生的问题,应该在一个while循环中使用wait()方法来等待通知,而不是使用if语句。在等待之前,线程应该检查等待条件,并在等待条件发生变化之前循环等待,以防止在虚假唤醒的情况下继续执行。 虚假唤醒是多线程编程中需要注意的一个陷阱。为了避免虚假唤醒产生的问题,可以使用锁对象的条件变量来处理线程的等待和唤醒操作,如使用ReentrantLock类中的Condition对象。这样可以更精确地控制线程的等待和唤醒,避免虚假唤醒问题
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值