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

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

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

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

多线程环境下,有多个线程执行了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条件只判断一次的尴尬情况。

目录
相关文章
|
搜索推荐
ChatGPT将会成为强者的外挂?—— 提高学习能力
ChatGPT将会成为强者的外挂?—— 提高学习能力
174 0
|
4月前
|
测试技术 UED 开发者
软件测试的艺术:从代码审查到用户反馈的全景探索在软件开发的宇宙中,测试是那颗确保星系正常运转的暗物质。它或许不总是站在聚光灯下,但无疑是支撑整个系统稳定性与可靠性的基石。《软件测试的艺术:从代码审查到用户反馈的全景探索》一文,旨在揭开软件测试这一神秘面纱,通过深入浅出的方式,引领读者穿梭于测试的各个环节,从细微处着眼,至宏观视角俯瞰,全方位解析如何打造无懈可击的软件产品。
本文以“软件测试的艺术”为核心,创新性地将技术深度与通俗易懂的语言风格相结合,绘制了一幅从代码审查到用户反馈全过程的测试蓝图。不同于常规摘要的枯燥概述,这里更像是一段旅程的预告片,承诺带领读者经历一场从微观世界到宏观视野的探索之旅,揭示每一个测试环节背后的哲学与实践智慧,让即便是非专业人士也能领略到软件测试的魅力所在,并从中获取实用的启示。
|
5月前
|
缓存 监控 数据可视化
秒杀系统背后的隐形杀手:日志处理的挑战与对策!
【8月更文挑战第21天】秒杀系统在短时间内需应对巨量用户访问,考验着系统的极限。本文聚焦秒杀场景下的日志处理挑战及优化策略。传统同步日志写入在高并发时易成瓶颈,拖慢服务响应。通过采用异步写入、利用内存缓冲,并结合ELK堆栈或云日志服务,可大幅减轻磁盘I/O压力。同时,合理设置日志级别,减少冗余信息,进一步提升系统效率。这些措施有助于构建更健壮的秒杀系统。
97 0
|
8月前
|
编解码 安全 定位技术
典型崩溃问题集锦
典型崩溃问题集锦
55 0
|
监控 前端开发
揭秘跨部门沟通的秘密武器:让不归你管的人主动配合你的绝妙方法!
揭秘跨部门沟通的秘密武器:让不归你管的人主动配合你的绝妙方法!
134 0
|
设计模式 小程序 测试技术
面对复杂问题时,系统思考助你理解问题本质
面对复杂问题时,系统思考助你理解问题本质
256 0
|
安全 网络安全 数据安全/隐私保护
面对未来网络安全 如何做到一劳永逸?
面对未来网络安全 如何做到一劳永逸?
面对未来网络安全 如何做到一劳永逸?
相亲源码开发,如何让消息模块发挥应有价值?
相亲源码开发,如何让消息模块发挥应有价值?
|
Kubernetes 监控 Cloud Native
面对不可避免的故障,我们造了一个“上帝视角”的控制台
混沌工程随着云原生的发展逐渐进入大家的视野,通过混沌工程可以很好地发现和解决在云原生化过程中的高可用问题。阿里巴巴在 2019 年开源了底层的混沌工程工具 - chaosblade,今年年初再次开源混沌工程控制台 chaosblade-box,ChaosBlade 品牌进一步升级。本文主要围绕云原生面临的高可用挑战和混沌工程机遇,详细介绍开源控制台的设计、特性和实践和未来规划,旨在帮助企业更好的了解控制台并通过其来实现混沌工程落地,解决云原生系统下高可用问题。
面对不可避免的故障,我们造了一个“上帝视角”的控制台
|
Web App开发 XML 安全
技巧:你未必知道的IE8九大功能
微软为IE8赋予了不少新的功能,其中一个就是使得这些新功能的实现更加实用和简便。其中有很多优化功能,可能你没有完全留意到。不过,你完全不用去阅读那些详细的功能使用说明,我们在为你提供这些功能介绍的同时,还将补充一个微软都没有提到过的重要技巧——如何为IE8增添强大的广告过滤工具。
974 0