首页 > 其他分享 >深入探究多线程中的虚假唤醒现象--从生产者消费者问题到高级解决方案的全方位解读

深入探究多线程中的虚假唤醒现象--从生产者消费者问题到高级解决方案的全方位解读

时间:2023-12-28 11:06:52浏览次数:31  
标签:执行 -- number int num 线程 多线程 唤醒



文章目录

  • 生产者和消费者问题
  • 虚假呼唤问题解决方案


线程之间的虚假唤醒问题常出现在多线程编程中。我看国内很多教程都解释的稀里糊涂的,所以打算写一篇博客好好絮叨絮叨。

首先看一下线程虚假唤醒的定义:

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

接下来我们用一个例子去详细上面这个解释,因为你看这个解释可能已经看蒙了。

生产者和消费者问题

  • 我们定义A、C线程为生产者,负责num+1
//A:num+1
        new Thread(()->{
            for (int i=0;i<10;i++){
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();

        //C:num+1
        new Thread(()->{
            for (int i=0;i<10;i++){
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"C").start();
  • 我们定义B、D线程为消费者,负责num-1
//B:num-1
        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.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"D").start();
  • 我们定义num为临界区共享资源,由生产者和消费者读写
private int number=0;

我们完整写下来就是:

package org.example;

/**
 * @author linghu
 * @date 2023/12/16 16:45
 * A num+1
 * B num-1
 * 顺序:判断->业务->通知
 */
public class A {
    public static void main(String[] args) {
        Data data = new Data();

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

        //B:num-1
        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();
    }
}

class Data{
    private int number=0;

    //+1
    public synchronized void increment() throws InterruptedException {
        if (number!=0){
            //等待
            this.wait();
        }
        number++;
        System.out.println(Thread.currentThread().getName()+"=>"+number);
        //通知其他线程,我-1完毕了
        this.notify();
    }

    //-1
    public synchronized void decrement() throws InterruptedException {
        if(number==0){
            //等待
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName()+"=>"+number);
        //通知其他线程,我-1完毕了
        this.notify();
    }


}

在上面代码中, incrementdecrement分别做加法和减法操作。这两个操作由四个线程ABCD去执行,A、C线程执行加法,B、D线程执行减法。

我们执行上面的代码会发生如下:

深入探究多线程中的虚假唤醒现象--从生产者消费者问题到高级解决方案的全方位解读_Data

上图发现,C、D线程执行下来已经出现了-1、、、。这就是我们说的线程虚假唤醒问题

线程虚假唤醒问题即:

A先执行,执行时调用了wait方法,那它会等待,此时会释放锁,那么线程B获得锁并且也会执行wait方法,两个加线程一起等待被唤醒。此时减线程中的某一个线程执行完毕并且唤醒了这俩加线程,那么这俩加线程不会一起执行,其中A获取了锁并且加1,执行完毕之后B再执行。如果是if的话,那么A修改完num后,B不会再去判断num的值,直接会给num+1。如果是while的话,A执行完之后,B还会去判断num的值,因此就不会执行。

这里的重点是: wait()以后会线程会释放锁!由于我们上面用的 if条件判断 number的值,所以A线程被唤醒执行完毕以后,轮到C线程开始执行的时候,C线程就会跳过下面这个判断:

if(number==0){
            //等待
            this.wait();
        }

直接执行如下代码:

//-1
    public synchronized void decrement() throws InterruptedException {
        if(number==0){
            //等待
            this.wait();
        }
        //上面的判断直接跳过
        //直接执行如下代码....
        number--;
        System.out.println(Thread.currentThread().getName()+"=>"+number);
        //通知其他线程,我-1完毕了
        this.notify();
    }

是的,问题就在于这个 if判断,导致了线程虚假唤醒。

我们在明确一下上面的结论:

AC线程负责去做加法,首先会判断num的值,如果num不为0,那么两个线程就开始等待,释放锁,这个时候CD线程获得锁去做减法,也会判断num的值,num的值如果不为0.然后开始做减法,做完减法就开始呼唤AC线程。AC线程被呼唤以后,A线程执行完毕,这个时候由于C线程中用了if判断,那么C线程执行的时候,就不会执行if判断了,于是导致了上面的线程虚假唤醒问题。

虚假呼唤问题解决方案

其实就是把上面线程执行的加法减法方法中的条件if改成while即可:

package org.example;

/**
 * @author linghu
 * @date 2023/12/16 16:45
 * A num+1
 * B num-1
 * 顺序:判断->业务->通知
 */
public class A {
    public static void main(String[] args) {
        Data data = new Data();
        //A:num+1
        new Thread(()->{
            for (int i=0;i<10;i++){
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();

        //B:num-1
        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();
    }
}

class Data{
    private int number=0;

    //+1
    public synchronized void increment() throws InterruptedException {
        while (number!=0){
            //等待
            this.wait();
        }
        number++;
        System.out.println(Thread.currentThread().getName()+"=>"+number);
        //通知其他线程,我-1完毕了
        this.notifyAll();
    }

    //-1
    public synchronized void decrement() throws InterruptedException {
        while (number==0){
            //等待
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName()+"=>"+number);
        //通知其他线程,我-1完毕了
        this.notifyAll();
    }


}

改成 while循环以后,A执行线程完毕以后释放锁,C线程才会继续执行while里的判断,这样就避免了if条件只判断一次的尴尬情况。


标签:执行,--,number,int,num,线程,多线程,唤醒
From: https://blog.51cto.com/u_15416819/9010977

相关文章

  • 基于ssm“魅力”繁峙宣传网站的设计与实现
    随着科学技术的飞速发展,各行各业都在努力与现代先进技术接轨,通过科技手段提高自身的优势;“魅力”繁峙宣传网站系统当然也不能排除在外,随着网络技术的不断成熟,带动了“魅力”繁峙宣传网站系统的发展,它彻底改变了过去传统的管理方式,不仅使服务管理难度变低了,还提升了管理的灵活性。这......
  • 【Java核心基础】Java中foreach方法核心总结
    Java中的foreach循环适用于多种场景:遍历数组、集合框架、处理列表数据等,可替代传统for循环,使代码更简洁。结合Lambda表达式和StreamAPI,可实现声明式数据处理,虽然不能直接遍历Map的键值对,但可遍历键或值集合,在多线程环境下,结合并行流使用foreach可并行处理数据。使用foreach能简......
  • Docker 部署 Prometheus Webhook DingTalk
    介绍在此部分简要介绍PrometheusWebhookDingTalk的作用和使用Docker部署的优势。概述将要涵盖的常用参数以及如何配置Docker容器的关键概念。步骤1:获取PrometheusWebhookDingTalk代码解释如何获取PrometheusWebhookDingTalk的代码并进入存储库目录。gitcloneh......
  • 软件测试的需求有哪些
    在软件开发过程中,有多种类型的需求,其中:(1)用户需求用于描述用户使用产品必须要完成的任务,是软件开发活动中最基本的需求。(2)系统需求用于描述软件设计和编程人员必须完成的任务,系统分析员通过分析用户需求,才能将用户需求转变成开发设计人员看得懂的系统需求。(3)测试需求用于描述软件测......
  • 基于SSM的宠物领养系统
    本课题是根据用户的需要以及网络的优势建立的一个宠物领养系统,来满足用宠物领养的需求。本宠物领养系统应用JSP技术,Java语言,MYSQL数据库存储数据,基于B/S结构开发。在网站的整个开发过程中,首先对系统进行了需求分析,设计出系统的主要功能模块,其次对网站进行总体规划和详细设计,最后对......
  • 泛互联网行业A/B测试全解析:产品优化的创新之道
    更多技术交流、求职机会,欢迎关注字节跳动数据平台微信公众号,回复【1】进入官方交流群近期,火山引擎数智平台举办了“超话数据:企业产品优化分享”的活动。火山引擎产品解决方案专家从企业应用的视角,分享了A/B实验在产品全用户生命周期的体验优化和案例。在用户拉新环节,企业可以通过广......
  • 2023十大前沿科技趋势揭秘!看看有你熟悉的领域吗?(下)
    点击查看原文:2023十大前沿科技趋势揭秘!看看有你熟悉的领域吗?(上)原创|文BFT机器人-Part3 空间计算-06 空间计算概念清晰消费级产品问世,XR全栈链路贯通空间计算,以其独特的计算维度,赋予了内容更加立体的呈现方式,近乎完美地再现了信息。业内外一致认为,空间计算将是下一代主要的计算......
  • 【AI编程工具】目前市面上常见的AI代码助手(AI Coding Assistant)
    目前市面上常见的AI代码助手(AICodingAssistant)有:GithubCopilot:提供更高效的代码编写、学习新的语言和框架以及更快的调试通义灵码_智能编码助手_AI编程_人工智能-阿里云AmazonCodeWhisper:实时代码建议CodeGeeX:国产免费编程AI助手iFlyCode:科大讯飞发布的编程新时代的智能助手Com......
  • 【Kafka-Eagle】EFAK告警配置与实践
    Kafka-Eagle是一个开源的Kafka集群监控与告警系统,可以帮助用户实现对Kafka集群的实时监控、性能指标收集以及异常告警等功能。下面是关于Kafka-Eagle的告警配置和实践的一般步骤:安装和配置Kafka-Eagle:下载最新版本的Kafka-Eagle安装包,并解压到一个合适的目录中。进入Kafka-Eagle的......
  • 【Spring】SpringMVC项目升级成SpringBoot实践
    将SpringMVC项目升级为SpringBoot项目需要一系列详细的步骤。以下是一个更详细的步骤指南:项目初始化:创建一个新的SpringBoot项目。您可以使用SpringInitializr或SpringBoot的Maven插件来快速生成项目结构。依赖管理:在新项目中,添加所需的依赖。根据您的项目需求,添加SpringBoot......