当前位置: 首页 > news >正文

JavaEE-多线程编程阻塞队列

目录

生产者消费者模型

生产者消费者模型优势

通过代码看一下生产者消费者模型(使用阻塞队列)

自己实现阻塞队列


之前在数据结构中学的队列是最基础的队列,在实际开发中针对队列还有很多形式:(1)普通队列 ,线程不安全 (2)优先级队列:以堆为基础的数据结构,线程不安全 (3)阻塞队列:线程安全,先进先出,带有阻塞功能。当队列为空时尝试出队操作就会阻塞等待,一直阻塞到队列不为空为止。同理,当队列为满时尝试入队列也会阻塞等待,阻塞到队列不为满为止。BlockingQueue就是标准库提供的阻塞队列。(4)消息队列——生产者消费者模型

生产者消费者模型

由于消息队列这样的数据结构特别好用,实际开发中经常把这样的数据结构封装成单独的服务器程序中单独部署,这样的服务器程序同样也称为消息队列

消息队列能起到的作用——生产者消费者模型

生产者消费者模型在开发中主要有两方面的意义:

(1)能让程序解耦合  (2)能够使程序“削峰填谷”

普通的队列也可以实现这个模型,具体要看实现的场景,如果需要在一个进程内实现生产者消费者模型的话直接使用阻塞队列即可,如果需要在分布式系统实现生产者消费者模型那就需要单独部署的消息队列服务器。

生产者消费者模型是一种典型的解决多线程问题的方式。比如说:当ABC三人分工合作包饺子,包饺子需要先擀皮再包,但擀面杖只有一个,ABC三人难免会去竞争擀面杖,此时用专业术语来讲就是“锁竞争”,如果分配A来擀皮,擀完的皮放到面板上让B和C来包,这个时候A就是生产者,B和C就是消费者,面板就相当于是阻塞队列/消息队列;如果A擀的块,B和C包得慢,当面板放满的时候A就陷入阻塞等待,B和C在面板上消费后A才能继续生产,同理B和C包的比A擀得快的话,BC就会阻塞等待A生产,A->面板->BC此时就是生产者消费者模型。

生产者消费者模型优势

生产者消费者模型有很多优势,最主要的两个:

(1)解耦合

当有线程A和线程B,让A直接调用B意味着A代码中要包含很多B的逻辑,B代码里也要包含A相关的逻辑,彼此间会有一定的耦合。一旦对A进行修改就可能会对B造成影响,一旦A出bug也可能会牵连到B,这种情况下的模型可以简单画为:

但如果采用生产者消费者模型,在AB中间加一块“面板”,此时可以画为:

此时站在A的视角,只需要关心和队列的交互,站在B的视角也只需要关心和队列的交互。如果对A的代码进行修改就不太能影响到B,大大降低了AB线程之间的耦合,未来如果引入别的线程也不需要对A进行修改,直接让新线程从队列中读取数据就可以。提高了代码的可扩展能力

(2)削峰填谷

客户端发来的请求数量多少没法提前预知,遇到突发事件可能会导致客户端发来的请求激增。A来接受客户端发来的大量请求,然后将请求发到消息队列,B从消息队列获取任务去执行,无论A给队列写多快,B都能按照自己的节奏消费数据,相当于队列将B保护了起来。

正常情况下,A收到一个客户端请求也会请求一次B,A收到的请求激增了B的请求也会激增,A的工作比较简单消耗资源少,B的工作更复杂消耗的资源更多,一旦请求多了就容易挂。

挂:服务器每次处理一个请求都要消耗一定的系统资源,如果同一时刻要处理的请求太多,消耗的总资源数目超出机器能提供的上线,机器就挂了)

三峡大坝起到的效果就是削峰填谷。

通过代码看一下生产者消费者模型(使用阻塞队列)

使用标准库提供的BlockingQueue

ArrayBlockingQueue->使用数组

LinkedBlockingQueue->链表

PriorityBlockingQueue->堆

主要使用put&take,其他的offer&add&poll是继承原队列的方法,不带有阻塞功能,看BlockingQueue的源码可以看到继承了Queue接口。

自己实现阻塞队列

阻塞队列是线程安全带有阻塞功能的普通队列,所以自行实现类似于阻塞队列的方法时可以用以下几步:(1)写一个普通队列 (2)加上线程安全 (3)加上阻塞功能

第一步:使用数组实现普通队列,用两个变量记录队首队尾的下标,还需引入一个变量记录当前数组的元素个数,记录满/空。第一步的代码写完后为:

class MyBlockingQueue{private int[] array = null;private int head;private int tail;private int size;void put(int num){if(array.length >= size ){ return; //当队列为满时入队操作应该阻塞等待,后续步骤再进行修改}array[tail] = num;tail++;if(tail >= array.length){tail = 0;}size++;}int take(){if(size == 0){return 0; //当队列为空时出队列应该阻塞等待,我们先完成第一步,后续再进行修改}int ret = array[head];head++;if(head >= array.length){head = 0;}size--;return ret;}}

第二步:将线程安全和阻塞的逻辑加上,代码为:

class MyBlockingQueue{private int[] array = null;private volatile int head;private volatile int tail;private volatile int size;void put(int num) throws InterruptedException {synchronized (this){ //(2)给关键操作加锁,加上线程安全if(array.length >= size ){this.wait(); //(1)当队列满时,当前线程进入等待,当队列被take时解除等待}}array[tail] = num;tail++;if(tail >= array.length){tail = 0;}size++;this.notify();}int take() throws InterruptedException {synchronized (this){if(size == 0){this.wait();}}int ret = array[head];head++;if(head >= array.length){head = 0;}size--;this.notify();//(3)将阻塞中的线程唤醒return ret;}}

逻辑步骤已经在代码中进行表示

需要注意(1)唤醒时使用notifyall是不合理的,比如说有两个线程put时被阻塞了,一次take将所有等待线程唤醒了,一个线程可以put成功,而另一个put就要出bug了

(2)当鼠标放在wait上时可以看见系统给出的信息

大概意思是wait除了会被notify&notifyall唤醒,还会被别的interrupt唤醒,搭配while使用可以在被唤醒之后再次进行判定是否符合唤醒条件,是否该继续进行。将if改为while就是我们自己实现的MyBlockingQueue的最后一步优化。

最后优化完的代码为:

class MyBlockingQueue{private int[] array = null;private volatile int head;private volatile int tail;private volatile int size;void put(int num) {synchronized (this){ while(array.length >= size ){try {this.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}}array[tail] = num;tail++;if(tail >= array.length){tail = 0;}size++;this.notify();}int take() {synchronized (this){while(size == 0){try {this.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}}int ret = array[head];head++;if(head >= array.length){head = 0;}size--;this.notify();return ret;}}

本篇文章到这里就结束了,感谢观看。

下篇文章更新线程池相关内容

感谢观看

道阻且长,行则将至

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 探索之路——初识 Vue Router:构建单页面应用的完整指南
  • 网络空间安全专业怎么样,可通过哪些途径自学?
  • 【Redis】 Redis 列表指令指南
  • python open cv(图像处理的基本操作)
  • Spring Bean生命周期
  • 什么是单例模式?
  • 【两整数之和】python刷题记录
  • Go并发编程
  • 简单的位运算
  • 设计模式实战:社交网络平台的设计与实现
  • 03:【stm32】GPIO
  • 【STM32】“stm32f10x.h” 头文件的作用
  • iPhone怎么大批量删除照片:释放你的存储空间
  • 【第十天】进程和线程的区别 并行和并发有什么区别 解释一下用户态和核心态
  • C语言初阶(11)
  • IE9 : DOM Exception: INVALID_CHARACTER_ERR (5)
  • JS 中的深拷贝与浅拷贝
  • 《Javascript数据结构和算法》笔记-「字典和散列表」
  • 2017年终总结、随想
  • IDEA常用插件整理
  • JAVA_NIO系列——Channel和Buffer详解
  • Java反射-动态类加载和重新加载
  • js算法-归并排序(merge_sort)
  • Leetcode 27 Remove Element
  • MD5加密原理解析及OC版原理实现
  • Rancher如何对接Ceph-RBD块存储
  • 阿里云Kubernetes容器服务上体验Knative
  • 不上全站https的网站你们就等着被恶心死吧
  • 第十八天-企业应用架构模式-基本模式
  • 猴子数据域名防封接口降低小说被封的风险
  • 基于Vue2全家桶的移动端AppDEMO实现
  • 猫头鹰的深夜翻译:Java 2D Graphics, 简单的仿射变换
  • 设计模式走一遍---观察者模式
  • 事件委托的小应用
  • 适配mpvue平台的的微信小程序日历组件mpvue-calendar
  • 问:在指定的JSON数据中(最外层是数组)根据指定条件拿到匹配到的结果
  • 用Python写一份独特的元宵节祝福
  • ​linux启动进程的方式
  • # MySQL server 层和存储引擎层是怎么交互数据的?
  • (C语言)深入理解指针2之野指针与传值与传址与assert断言
  • (c语言版)滑动窗口 给定一个字符串,只包含字母和数字,按要求找出字符串中的最长(连续)子串的长度
  • (LeetCode) T14. Longest Common Prefix
  • (独孤九剑)--文件系统
  • (函数)颠倒字符串顺序(C语言)
  • (机器学习的矩阵)(向量、矩阵与多元线性回归)
  • (十六)Flask之蓝图
  • (原創) 未来三学期想要修的课 (日記)
  • (正则)提取页面里的img标签
  • *算法训练(leetcode)第三十九天 | 115. 不同的子序列、583. 两个字符串的删除操作、72. 编辑距离
  • .gitignore文件---让git自动忽略指定文件
  • .NET core 自定义过滤器 Filter 实现webapi RestFul 统一接口数据返回格式
  • .NET Core跨平台微服务学习资源
  • .net 简单实现MD5
  • .NET 设计一套高性能的弱事件机制
  • .NET 应用架构指导 V2 学习笔记(一) 软件架构的关键原则