高并发编程学习(2)——线程通信详解

2023-10-31

为获得良好的阅读体验,请访问原文: 传送门

前序文章

- 高并发编程学习(1)——并发基础 - https://www.wmyskxz.com/2019/11/26/gao-bing-fa-bian-cheng-xue-xi-1-bing-fa-ji-chu/

一、经典的生产者消费者案例

上一篇文章我们提到一个应用可以创建多个线程去执行不同的任务,如果这些任务之间有着某种关系,那么线程之间必须能够通信来协调完成工作。

生产者消费者问题(英语:Producer-consumer problem)就是典型的多线程同步案例,它也被称为有限缓冲问题(英语:Bounded-buffer problem)。该问题描述了共享固定大小缓冲区的两个线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。(摘自维基百科:生产者消费者问题)

  • 注意: 生产者-消费者模式中的内存缓存区的主要功能是数据在多线程间的共享,此外,通过该缓冲区,可以缓解生产者和消费者的性能差;

准备基础代码:无通信的生产者消费者

我们来自己编写一个例子:一个生产者,一个消费者,并且让他们让他们使用同一个共享资源,并且我们期望的是生产者生产一条放到共享资源中,消费者就会对应地消费一条。

我们先来模拟一个简单的共享资源对象:

public class ShareResource {

    private String name;
    private String gender;

    /**
     * 模拟生产者向共享资源对象中存储数据
     *
     * @param name
     * @param gender
     */
    public void push(String name, String gender) {
        this.name = name;
        this.gender = gender;
    }

    /**
     * 模拟消费者从共享资源中取出数据
     */
    public void popup() {
        System.out.println(this.name   "-"   this.gender);
    }
}

然后来编写我们的生产者,使用循环来交替地向共享资源中添加不同的数据:

public class Producer implements Runnable {

    private ShareResource shareResource;

    public Producer(ShareResource shareResource) {
        this.shareResource = shareResource;
    }

    @Override
    public void run() {
        for (int i = 0; i < 50; i  ) {
            if (i % 2 == 0) {
                shareResource.push("凤姐", "女");
            } else {
                shareResource.push("张三", "男");
            }
        }
    }
}

接着让我们的消费者不停地消费生产者产生的数据:

public class Consumer implements Runnable {

    private ShareResource shareResource;

    public Consumer(ShareResource shareResource) {
        this.shareResource = shareResource;
    }

    @Override
    public void run() {
        for (int i = 0; i < 50; i  ) {
            shareResource.popup();
        }
    }
}

然后我们写一段测试代码,来看看效果:

public static void main(String[] args) {
    // 创建生产者和消费者的共享资源对象
    ShareResource shareResource = new ShareResource();
    // 启动生产者线程
    new Thread(new Producer(shareResource)).start();
    // 启动消费者线程
    new Thread(new Consumer(shareResource)).start();
}

我们运行发现出现了诡异的现象,所有的生产者都似乎消费到了同一条数据:

张三-男
张三-男
....以下全是张三-男....

为什么会出现这样的情况呢?照理说,我的生产者在交替地向共享资源中生产数据,消费者也应该交替消费才对呀..我们大胆猜测一下,会不会是因为消费者是直接循环了 30 次打印共享资源中的数据,而此时生产者还没有来得及更新共享资源中的数据,消费者就已经连续打印了 30 次了,所以我们让消费者消费的时候以及生产者生产的时候都小睡个 10 ms 来缓解消费太快 or 生产太快带来的影响,也让现象更明显一些:

/**
 * 模拟生产者向共享资源对象中存储数据
 *
 * @param name
 * @param gender
 */
public void push(String name, String gender) {
    try {
        Thread.sleep(10);
    } catch (InterruptedException ignored) {
    }
    this.name = name;
    this.gender = gender;
}

/**
 * 模拟消费者从共享资源中取出数据
 */
public void popup() {
    try {
        Thread.sleep(10);
    } catch (InterruptedException ignored) {
    }
    System.out.println(this.name   "-"   this.gender);
}

再次运行代码,发现了出现了以下的几种情况:

  • 重复消费:消费者连续地出现两次相同的消费情况(张三-男/ 张三-男);
  • 性别紊乱:消费者消费到了脏数据(张三-女/ 凤姐-男);

分析出现问题的原因

  • 重复消费:我们先来看看重复消费的问题,当生产者生产出一条数据的时候,消费者正确地消费了一条,但是当消费者再来共享资源中消费的时候,生产者还没有准备好新的一条数据,所以消费者就又消费到老数据了,这其中的根本原因是生产者和消费者的速率不一致
  • 性别紊乱:再来分析第二种情况。不同于上面的情况,消费者在消费第二条数据时,生产者也正在生产新的数据,但是尴尬的是,生产者只生产了一半儿(也就是该执行完 this.name = name),也就是还没有来得及给 gender 赋值就被消费者给取走消费了.. 造成这样情况的根本原因是没有保证生产者生产数据的原子性

解决出现的问题

加锁解决性别紊乱

我们先来解决性别紊乱,也就是原子性的问题吧,上一篇文章里我们也提到了,对于这样的原子性操作,解决方法也很简单:加锁。稍微改造一下就好了:

/**
 * 模拟生产者向共享资源对象中存储数据
 *
 * @param name
 * @param gender
 */
synchronized public void push(String name, String gender) {
    this.name = name;
    try {
        Thread.sleep(10);
    } catch (InterruptedException ignored) {
    }
    this.gender = gender;
}

/**
 * 模拟消费者从共享资源中取出数据
 */
synchronized public void popup() {
    try {
        Thread.sleep(10);
    } catch (InterruptedException ignored) {
    }
    System.out.println(this.name   "-"   this.gender);
}

  • 我们在方法前面都加上了 synchronized 关键字,来保证每一次读取和修改都只能是一个线程,这是因为当 synchronized 修饰在普通同步方法上时,它会自动锁住当前实例对象,也就是说这样改造之后读/ 写操作同时只能进行其一;
  • 我把 push 方法小睡的代码改在了赋值 namegender 的中间,以强化验证原子性操作是否成功,因为如果不是原子性的话,就很可能出现赋值 name 还没赋值给 gender 就被取走的情况,小睡一会儿是为了加强这种情况的出现概率(可以试着把 synchronized 去掉看看效果);

运行代码后发现,并没有出现性别紊乱的现象了,但是重复消费仍然存在。

等待唤醒机制解决重复消费

我们期望的是 张三-男凤姐-女 交替出现,而不是有重复消费的情况,所以我们的生产者和消费者之间需要一点沟通,最容易想到的解决方法是,我们新增加一个标志位,然后在消费者中使用 while 循环判断,不满足条件则不消费,条件满足则退出 while 循环,从而完成消费者的工作。

while (value != desire) {
    Thread.sleep(10);
}
doSomething();

这样做的目的就是为了防止「过快的无效尝试」,这种方法看似能够实现所

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

高并发编程学习(2)——线程通信详解 的相关文章

随机推荐

  • springboot定时任务

    1 配置 在主函数加 EnableScheduling 定时任务 package com biubiu import org springframework boot SpringApplication import org springf
  • golang的web框架Gin(一)---Gin的安装与初体验

    简介 1 1 介绍 Go世界里最流行的Web框架 Github上有32K star 基于httprouter开发的Web框架 中文文档齐全 简单易用的轻量级框架 Gin是一个golang的微框架 封装比较优雅 API友好 源码注释比较明确
  • [已解决]运行 ‘tomcat8‘ 出错: 无法打开调试器端口 (127.0.0.1:6672): java.net.SocketException

    解决 运行 tomcat8 出错 无法打开调试器端口 127 0 0 1 6672 java net SocketException 修改HTTP port端口号 没占用的端口号都可以 建议8000以上的数字 只要不是现在的端口号就可以 修
  • ARM(IMX6U)裸机按键输入实验(BSP+SDK、GPIO输入与输出、按键消抖)

    参考 Linux之ARM IMX6U 裸机按键输入实验 GPIO的输出与输入 作者 一只青木呀 发布时间 2020 08 17 21 43 37 网址 https blog csdn net weixin 45309916 article
  • Python实现顺序表

    Python实现顺序表 关于顺序表的介绍 请参考 https blog csdn net weixin 43790276 article details 103848039 Python 中的列表和元组都属于顺序表 下面根据顺序表的特性 自
  • 段错误的调试方法(printf输出、GDB)

    参考 段错误产生原因及简单的调试方法 参考 如何解决段错误 参考 C语言gdb调试之精髓 常用命令 多进程 多线程 程序日志 网址 https www bilibili com video BV1ei4y1V758 from search
  • 1031. 查验身份证(15)

    一个合法的身份证号码由17位地区 日期编号和顺序编号加1位校验码组成 校验码的计算规则如下 首先对前17位数字加权求和 权重分配为 7 9 10 5 8 4 2 1 6 3 7 9 10 5 8 4 2 然后将计算的和对11取模得到值Z 最
  • 艺赛旗RPA--经验分享:Python 中的“特殊”函数

    了解RPA www i search com cn 学习RPA https support i search com cn 私有函数 魔法函数 回调函数 在任何编程语言中 都会规定某些对象 属性 方法 函数 类等 只能够在某个范围内访问 出
  • Linux MISC 驱动实验

    目录 MISC 设备驱动简介 硬件原理图分析 实验程序编写 修改设备树 beep 驱动程序编写 编写测试APP 运行测试 编译驱动程序和测试APP 运行测试 misc 的意思是混合 杂项的 因此MISC 驱动也叫做杂项驱动 也就是当我们板子
  • Docker使用docker compose部署zfile 实现在线浏览下载linux中的文件

    gt Docker及docker compose的安装点这里 创建 docker compose yml 文件 version 3 services zfile image stilleshan zfile container name z
  • Eudcoder--Java面向对象(第五章)- 包装类

    大家好啊 好久不见 新的一期来啦 让我们一起学习 快来 教你一个解除部分网课平台关于复制粘贴限制的方法 第一题 请仔细阅读右侧代码 根据方法内的提示 在Begin End区域内进行代码补充 具体任务如下 完成基本数据类型与包装类之间的相互转
  • echarts.common.min.js:22 Uncaught Error: Component series.bar  not exists. Load it first

    一 统计表无法显示 浏览器控制台报错 echarts common min js 22 Uncaught Error Component series bar not exists Load it first at Function Vn
  • 数据的存储方式(Parquet、ORC)

    文章目录 数据的存储方式 按行存储 按列存储 Parquest 文件布局 概念 并行处理的单元 配置 Row Group Size 行组的大小 Data Page Size 数据页的大小 元数据 数据页 Hive下的Parquet实验 Pa
  • postgresql:字符串累加拼接(聚合分组拼接)

    问题 有时 想要将某字段在查询列表的时候 按分组的不同 同组字符串累加拼接起来 原表数据内容如下 想要达到的目标结果 是把cdate tno的字符串分组累加拼接起来 如下 解决方案 使用聚合函数 string agg 示例如下 SELECT
  • 基于self-attention的BILSTM时间序列预测Python程序

    基于self attention的BILSTM时间序列预测Python程序 特色 1 单变量 多变量输入 自由切换 2 单步预测 多步预测 自动切换 3 基于Pytorch架构 4 多个评估指标 MAE MSE R2 MAPE等 5 数据从
  • 微信公众号测试号url和token绑定失败解决问题

    前提准备 在本地搭建一个本地服务器 具体查看如何搭建一个本地服务器 首先 我们需要到natapp获取一个信道 博主这里买的是vip1型的 当然也可以使用免费型的 根据需要选择 完了之后 去 我的隧道 查看购买的信道 复制里面的authtok
  • Java之包装类的算法小题的练习

    算法小题 练习一 需求 键盘录入一些1 10日之间的整数 并添加到集合中 直到集合中所有数据和超过200为止 代码示例 public class Test1 public static void main String args 键盘录入一
  • 修改Git远程地址 git config remote.origin.url "https://..."

    仓库管理 添加或指定远程仓库地址 git remote set url origin https git config remote origin url https 删除 git remote rm origin
  • python基础--函数入门与进阶

    函数入门与进阶 函数参数的使用 位置参数 关键字参数 默认参数 可变参数 关键字可变参数 函数的相互调用 函数的作用域 全局作用域 局部作用域 数据的打包与拆包 数据打包 数据的拆包 lambda函数 递归 前言 此专栏文章是专门针对Pyt
  • 高并发编程学习(2)——线程通信详解

    为获得良好的阅读体验 请访问原文 传送门 前序文章 高并发编程学习 1 并发基础 https www wmyskxz com 2019 11 26 gao bing fa bian cheng xue xi 1 bing fa ji chu