《从生产者消费者问题到高级解决方案的全方位解读&探究虚假呼唤现象》

简介: 《从生产者消费者问题到高级解决方案的全方位解读&探究虚假呼唤现象》

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

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

多线程环境下,有多个线程执行了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线程执行减法。

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

上图发现,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条件只判断一次的尴尬情况。

目录
相关文章
|
1月前
|
机器学习/深度学习 人工智能 监控
提升软件质量的关键路径:高效测试策略与实践在软件开发的宇宙中,每一行代码都如同星辰般璀璨,而将这些星辰编织成星系的过程,则依赖于严谨而高效的测试策略。本文将引领读者探索软件测试的奥秘,揭示如何通过精心设计的测试方案,不仅提升软件的性能与稳定性,还能加速产品上市的步伐,最终实现质量与效率的双重飞跃。
在软件工程的浩瀚星海中,测试不仅是发现缺陷的放大镜,更是保障软件质量的坚固防线。本文旨在探讨一种高效且创新的软件测试策略框架,它融合了传统方法的精髓与现代技术的突破,旨在为软件开发团队提供一套系统化、可执行性强的测试指引。我们将从测试规划的起点出发,沿着测试设计、执行、反馈再到持续优化的轨迹,逐步展开论述。每一步都强调实用性与前瞻性相结合,确保测试活动能够紧跟软件开发的步伐,及时适应变化,有效应对各种挑战。
|
2月前
|
测试技术 UED 开发者
软件测试的艺术:从代码审查到用户反馈的全景探索在软件开发的宇宙中,测试是那颗确保星系正常运转的暗物质。它或许不总是站在聚光灯下,但无疑是支撑整个系统稳定性与可靠性的基石。《软件测试的艺术:从代码审查到用户反馈的全景探索》一文,旨在揭开软件测试这一神秘面纱,通过深入浅出的方式,引领读者穿梭于测试的各个环节,从细微处着眼,至宏观视角俯瞰,全方位解析如何打造无懈可击的软件产品。
本文以“软件测试的艺术”为核心,创新性地将技术深度与通俗易懂的语言风格相结合,绘制了一幅从代码审查到用户反馈全过程的测试蓝图。不同于常规摘要的枯燥概述,这里更像是一段旅程的预告片,承诺带领读者经历一场从微观世界到宏观视野的探索之旅,揭示每一个测试环节背后的哲学与实践智慧,让即便是非专业人士也能领略到软件测试的魅力所在,并从中获取实用的启示。
|
3月前
|
搜索推荐 知识图谱 UED
信息检索新技术问题之回音室效应的定义如何解决
信息检索新技术问题之回音室效应的定义如何解决
28 0
|
3月前
|
机器学习/深度学习 人工智能 监控
🎯目标精准打击:AI助力职场项目管理,确保每个细节都完美执行!
【8月更文挑战第1天】在快节奏职场中,项目管理至关重要。AI技术快速发展,正深度融入项目管理,以其卓越的数据处理和智能分析能力,助力团队精准设定目标并完美执行细节。通过分析历史数据和市场趋势,AI支持数据驱动的决策,预测风险和资源需求,确保目标既挑战又可行。在执行阶段,AI实时监控项目进展,及时预警并提供建议,自动化处理重复任务,使团队更专注于解决问题。AI的引入正引领项目管理走向更高效率、精确度和智能化的新时代。
73 0
|
监控 前端开发
揭秘跨部门沟通的秘密武器:让不归你管的人主动配合你的绝妙方法!
揭秘跨部门沟通的秘密武器:让不归你管的人主动配合你的绝妙方法!
114 0
|
5G 芯片
带你读《果壳中的5G:新网络时代的技术内涵与商业思维》第二章通信世界的第二次嬗变2.6
带你读《果壳中的5G:新网络时代的技术内涵与商业思维》第二章通信世界的第二次嬗变2.6
带你读《果壳中的5G:新网络时代的技术内涵与商业思维》第二章通信世界的第二次嬗变2.6
|
运维 5G
带你读《果壳中的5G:新网络时代的技术内涵与商业思维》第二章通信世界的第二次嬗变2.2(一)
《果壳中的5G:新网络时代的技术内涵与商业思维》第二章通信世界的第二次嬗变2.2(一)
带你读《果壳中的5G:新网络时代的技术内涵与商业思维》第二章通信世界的第二次嬗变2.2(一)
|
运维 安全 5G
带你读《果壳中的5G:新网络时代的技术内涵与商业思维》第二章通信世界的第二次嬗变2.5(二)
带你读《果壳中的5G:新网络时代的技术内涵与商业思维》第二章通信世界的第二次嬗变2.5
带你读《果壳中的5G:新网络时代的技术内涵与商业思维》第二章通信世界的第二次嬗变2.5(二)
|
存储 编解码 关系型数据库
带你读《果壳中的5G:新网络时代的技术内涵与商业思维》第二章通信世界的第二次嬗变2.3
带你读《果壳中的5G:新网络时代的技术内涵与商业思维》第二章通信世界的第二次嬗变2.3
带你读《果壳中的5G:新网络时代的技术内涵与商业思维》第二章通信世界的第二次嬗变2.3
|
运维 自动驾驶 物联网
带你读《果壳中的5G:新网络时代的技术内涵与商业思维》第二章通信世界的第二次嬗变2.1
《果壳中的5G:新网络时代的技术内涵与商业思维》第二章通信世界的第二次嬗变2.1
带你读《果壳中的5G:新网络时代的技术内涵与商业思维》第二章通信世界的第二次嬗变2.1