【Java多线程案例】实现阻塞队列

1. 阻塞队列简介

1.1 阻塞队列概念

阻塞队列:是一种特殊的队列,具有队列"先进先出"的特性,同时相较于普通队列,阻塞队列是线程安全的,并且带有阻塞功能,表现形式如下:

  • 当队列满时,继续入队列就会阻塞,直到有其他线程从队列中取出元素
  • 当队列空时,继续出队列就会阻塞,直到有其他线程往队列中插入元素

基于阻塞队列我们可以实现生产者消费者模型,这在后端开发场景中是相当重要的!

1.2 生产者-消费者模型优势

基于阻塞队列实现的 生产者消费者模型 具有以下两大优势:

  1. 解耦合:

image.png
以搜狗搜索的服务器举例,用户输入搜索关键字 **美容,**客户端的请求到达搜狗的"入口服务器"时,会将请求转发到 广告服务器大搜索服务器,此时广告服务器返回相关广告内容,大搜索服务器根据搜索算法匹配对应结果返回,如果按照这种方式通信,那么入口服务器需要编写两套代码分别同广告服务器和大搜索服务器进行交互,并且一个严重问题是如果其中广告服务器宕机了,会导致入口服务器无法正常工作进而影响大搜索服务器也无法正常工作!!
image.png
而引入阻塞队列后,入口服务器不需要知晓广告服务器和大搜索服务器的存在,只需要往阻塞队列中发送请求即可,而广告服务器和大搜索服务器也不需要知道入口服务器的存在,只需要从阻塞队列中取出请求处理完毕返回给阻塞队列即可,并且当其中大搜索服务器宕机时,不影响其他服务器以及入口服务器的正常运作!

  1. 削峰填谷:

image.png
如果没有阻塞队列,当遇到一些突发场景例如"双十一"大促等客户请求量激增的时候,入口服务器转发的请求量增多,压力就会变大,同理广告服务器和大搜索服务器处理过程复杂繁多,消耗的硬件资源就会激增,达到硬件瓶颈之后服务器就宕机了(直观现象就是客户端发送请求,服务器不会响应了)
image.png
而引入阻塞队列/消息队列之后,由于阻塞队列只负责存储相应的请求或者响应,无需额外的业务处理,因此抗压能力比广告服务器和大搜索服务器更强,当客户请求量激增的时候交由阻塞队列承受,而广告服务器和大搜索服务器只需要按照特定的速率进行读取并返回处理结果即可,就起到了 削峰填谷 的作用!

注意:此处的阻塞队列在现实场景中并不是一个单纯的数据结构,往往是一个基于阻塞队列的服务器程序,例如消息队列(MQ)

2. 标准库中的阻塞队列

2.1 基本介绍

Java标准库提供了现成的阻塞队列数据结构供开发者使用,即BlockingQueue接口
BlockingQueue:该接口具有以下实现类:

  1. ArrayBlockingQueue:基于数组实现的阻塞队列
  2. LinkedBlockingQueue:基于链表实现的阻塞队列
  3. PriorityBlockingQueue:带有优先级的阻塞队列

BlockingQueue方法:该接口具有以下常用方法

  1. 带有阻塞功能:
  • put:向队列中入元素,队列满则阻塞等待
  • take:向队列中取出元素,队列空则阻塞等待
  1. 不带有阻塞功能:
  • peek:返回队头元素(不取出)
  • poll:返回队头元素(取出)
  • offer:向队列中插入元素

2.2 代码示例

/*** 测试Java标准库提供的阻塞队列实现*/
public class TestStandardBlockingQueue {private static BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);public static void main(String[] args) {// 生产者Thread t1 = new Thread(() -> {int i = 0;while (true) {try {queue.put(i);System.out.println("生产数据:" + i);i++;} catch (InterruptedException e) {throw new RuntimeException(e);}}});// 消费者Thread t2 = new Thread(() -> {while (true) {try {Thread.sleep(1000);int ele = queue.take();System.out.println("消费数据:" + ele);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t1.start();t2.start();}
}

运行效果
image.png
我们在主线程中创建了两个线程,其中t1线程作为生产者不断循环生产元素,而线程t2作为消费者每隔1s消费一个数据,所以我们很快看到当生产数据个数达到容量capacity时就会继续生产就会阻塞等待,直到消费者线程消费数据后才可以继续入队列,这样就实现了一个 生产者-消费者模型

3. 自定义实现阻塞队列

首先我们需要明确实现一个阻塞队列需要哪些步骤?

  1. 首先我们需要实现一个普通队列
  2. 使用锁机制将普通队列变成线程安全的
  3. 通过特殊机制让该队列能够带有"阻塞"功能

3.1 实现普通队列

相信大家如果学过 数据结构与算法 相关课程,应该对队列这种数据结构的实现并不陌生!实现队列有基于数组的也有基于链表的,我们此处采用基于数组实现的,基于数组实现的循环队列也有以下两种方式:

  1. 腾出一个空间用来判断队列空或者满
  2. 使用额外的变量size用来记录当前元素的个数

我们使用第二种方式实现,实现代码如下:

/*** 自定义实现阻塞队列*/
public class MyBlockingQueue {private int head = 0; // 头指针private int tail = 0; // 尾指针private int size = 0; // 当前元素个数private String[] array = null;private int capacity; // 容量public MyBlockingQueue(int capacity) {this.capacity = capacity;this.array = new String[capacity];}/*** 入队列方法*/public void put(String elem) {if (size == capacity) {// 队列已经满了return;}array[tail] = elem;tail++;if (tail >= capacity) {tail = 0;}size++;}/*** 出队列方法*/public String take() {// 判断队列是否为空if (size == 0) {return null;}String topElem = array[head];head++;if (head >= capacity) {head = 0;}size--;return topElem;}public static void main(String[] args) {MyBlockingQueue queue = new MyBlockingQueue(3);queue.put("11");queue.put("22");queue.put("33");System.out.println(queue.take());System.out.println(queue.take());System.out.println(queue.take());}
}

3.2 引入锁机制实现线程安全

引入synchronized关键字在原有队列实现的基础上实现线程安全,代码如下:

/*** 自定义实现阻塞队列*/
public class MyBlockingQueue {private int head = 0; // 头指针private int tail = 0; // 尾指针private int size = 0; // 当前元素个数private String[] array = null;private int capacity; // 容量private Object locker = new Object(); // 锁对象public MyBlockingQueue(int capacity) {this.capacity = capacity;this.array = new String[capacity];}/*** 入队列方法*/public void put(String elem) {synchronized (locker) {if (size == capacity) {// 队列已经满了return;}array[tail] = elem;tail++;if (tail >= capacity) {tail = 0;}size++;}}/*** 出队列方法*/public String take() {String topElem = "";synchronized (locker) {// 判断队列是否为空if (size == 0) {return null;}topElem = array[head];head++;if (head >= capacity) {head = 0;}size--;}return topElem;}public static void main(String[] args) {MyBlockingQueue queue = new MyBlockingQueue(3);queue.put("11");queue.put("22");queue.put("33");System.out.println(queue.take());System.out.println(queue.take());System.out.println(queue.take());}
}

我们在puttake等关键方法上将 多个线程修改同一个变量 部分的操作进行加锁处理,实现线程安全!

3.3 加入阻塞功能

在普通队列的实现中,如果队列满或者空我们直接使用return关键字返回,但是在多线程环境下我们希望实现阻塞等待的功能,这就可以使用Object类提供的wait/notify这组方法实现阻塞与唤醒机制了!我们就需要考虑阻塞与唤醒的时机了!
何时阻塞:这个问题非常简单,当队列满时入队列操作就应该阻塞等待,而当队列为空时出队列操作就需要阻塞等待
何时唤醒:想必大家都可以想到,对于入队列操作来说,只要队列不满就可以被唤醒,而对于出队列操作来说,队列不为空就可以被唤醒,因此,只要有线程调用take操作出队列,那么入队列的线程就可以被唤醒,而只要有线程调用put操作入队列,那么出队列的线程就可以被唤醒

/*** 自定义实现阻塞队列*/
public class MyBlockingQueue {private int head = 0; // 头指针private int tail = 0; // 尾指针private int size = 0; // 当前元素个数private String[] array = null;private int capacity; // 容量private Object locker = new Object(); // 锁对象public MyBlockingQueue(int capacity) {this.capacity = capacity;this.array = new String[capacity];}/*** 入队列方法*/public void put(String elem) {synchronized (locker) {while (size == capacity) {// 队列已经满了(进行阻塞)try {locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}array[tail] = elem;tail++;if (tail >= capacity) {tail = 0;}size++;locker.notifyAll();}}/*** 出队列方法*/public String take() {String topElem = "";synchronized (locker) {// 判断队列是否为空while (size == 0) {try {locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}topElem = array[head];head++;if (head >= capacity) {head = 0;}size--;locker.notifyAll();}return topElem;}public static void main(String[] args) {MyBlockingQueue queue = new MyBlockingQueue(10);// 生产者Thread producer = new Thread(() -> {int i = 0;while (true) {queue.put(i + "");System.out.println("生产元素:" + i);i++;}});// 消费者Thread consumer = new Thread(() -> {while (true) {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}String elem = queue.take();System.out.println("消费元素" + elem);}});producer.start();consumer.start();}
}

我们使用wait/notify这组操作实现了阻塞/唤醒功能,并且满足必须使用在synchronized关键字内部的使用条件,这里有一个注意点

为什么我们将if判断条件改成了while循环呢???这是需要考虑清楚的!

image.png
如图所示:一开始由于队列满所以生产者1进入阻塞状态,释放锁,然后生产者2也进入阻塞状态释放锁,此时消费者消费一个元素后唤醒生产者1,然后生产者1生产一个元素后(记住此时队列已满)继续唤醒,但是此时唤醒的恰恰是 生产者2 ,生产者2继续执行生产元素,于是就出现问题,我们总结一下出现问题的原因:

  1. notifyAll是随机唤醒,无法指定唤醒线程,因此可能出现生产者唤醒生产者,消费者唤醒消费者的情况
  2. if判定条件一经执行就无法继续判定,所以生产者2被唤醒后没有再次判断当前队列是否满

于是我们的应对策略就是使用while循环,当线程被唤醒使重新判断,如果队列仍满,入队列操作继续阻塞,而队列仍空,出队列操作继续阻塞!Java标准也推荐我们使用 while 关键字和 wait 关键字一起使用!
image.png

4. 应用场景(实现生产者消费者模型)

我们继续基于我们自定义实现的阻塞队列再来实现 生产者-消费者模型
代码示例(主函数)

public static void main(String[] args) {MyBlockingQueue queue = new MyBlockingQueue(10);// 生产者Thread producer = new Thread(() -> {int i = 0;while (true) {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}queue.put(i + "");System.out.println("生产元素:" + i);i++;}});// 消费者Thread consumer = new Thread(() -> {while (true) {String elem = queue.take();System.out.println("消费元素" + elem);}});producer.start();consumer.start();
}

运行效果
image.png
此时我们创建两个两个线程,producer作为生产者线程每隔1s生产一个元素,consumer作为消费者线程不断消费元素,此时我们看到的就是消费者消费很快,当阻塞队列空时就进入阻塞状态,直到生产者线程生产元素后才被唤醒继续执行!此时我们真正模拟实现了 阻塞队列 这样的数据结构!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/259105.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MySQL数据库⑪_C/C++连接MySQL_发送请求

目录 1. 下载库文件 2. 使用库 3. 链接MySQL函数 4. C/C链接示例 5. 发送SQL请求 6. 获取查询结果 本篇完。 1. 下载库文件 要使用C/C连接MySQL&#xff0c;需要使用MySQL官网提供的库。 进入MySQL官网选择适合自己平台的mysql connect库&#xff0c;然后点击下载就行…

Codeforces Round 926 (Div. 2) C. Sasha and the Casino (Java)

Codeforces Round 926 (Div. 2) CC. Sasha and the Casino (Java) 比赛链接&#xff1a;Codeforces Round 926 (Div. 2) C题传送门&#xff1a;C. Sasha and the Casino 题目&#xff1a;C. Sasha and the Casino **Example ** input 2 1 7 2 1 1 2 3 15 3 3 6 4 4 5 5 4 7…

云计算基础-计算虚拟化-CPU虚拟化

CPU指令系统 在CPU的工作原理中&#xff0c;CPU有不同的指令集&#xff0c;如下图&#xff0c;CPU有4各指令集&#xff1a;Ring0-3&#xff0c;指令集是在服务器上运行的所有命令&#xff0c;最终都会在CPU上执行&#xff0c;但是CPU并不是说所有的命令都是一视同仁的&#xf…

【java】Hibernate访问数据库

一、Hibernate访问数据库案例 Hibernate 是一个在 Java 社区广泛使用的对象关系映射&#xff08;ORM&#xff09;工具。它简化了 Java 应用程序中数据库操作的复杂性&#xff0c;并提供了一个框架&#xff0c;用于将对象模型数据映射到传统的关系型数据库。下面是一个简单的使…

uniapp API文档地址 以及 HBuilder安装

uniapp API文档地址 以及 HBuilder安装 一、进入 当前网站 uni-app 官网 二、点击截图下载文件 三、下载HBuilder X 进入 当前网站 浏览器会识别 也可以自行选择版本四、直接点击Download For Windows 直接点击下载会下载压缩包 解压 后 双击截图内的 .exe五、以上上述完…

C++初阶:容器适配器介绍、stack和queue常用接口详解及模拟实现

介绍完了list类的相关内容后&#xff1a;C初阶&#xff1a;适合新手的手撕list&#xff08;模拟实现list&#xff09; 接下来进入新的篇章&#xff0c;stack和queue的介绍以及模拟&#xff1a; 文章目录 1.stack的初步介绍2.stack的使用3.queue的初步介绍4.queue的使用5.容器适…

Python六级考试笔记

Python六级考试笔记【源源老师】 六级标准 一、 掌握文件操作及数据格式化。 二、 掌握数据可视化操作。 三、 理解类与对象的概念&#xff0c;初步掌握类与对象的使用。 四、 掌握SQLite数据库基础编程。 五、 掌握简单的使用tkinter的GUI设计。 ​ 1. 文件操作 &#xff0…

每日OJ题_二叉树dfs①_力扣2331. 计算布尔二叉树的值

目录 力扣2331. 计算布尔二叉树的值 解析代码 力扣2331. 计算布尔二叉树的值 2331. 计算布尔二叉树的值 难度 简单 给你一棵 完整二叉树 的根&#xff0c;这棵树有以下特征&#xff1a; 叶子节点 要么值为 0 要么值为 1 &#xff0c;其中 0 表示 False &#xff0c;1 表示…

【C++】模版初阶

目录 泛函编程 函数模版 概念 格式 原理 实例化 模版函数的匹配原则 类模板 定义格式 泛函编程 如何实现一个通用的交换函数呢&#xff1f; void Swap(int& left, int& right) {int temp left;left right;right temp; } void Swap(double& left, dou…

数学建模:K-means聚类手肘法确定k值(含python实现)

原理 当K-means聚类的k值不被指定时&#xff0c;可以通过手肘法来估计聚类数量。   在聚类的过程中&#xff0c;随着聚类数的增大&#xff0c;样本划分会变得更加精细&#xff0c;每个类别的聚合程度更高&#xff0c;那么误差平方和&#xff08;SSE&#xff09;会逐渐变小&am…

上门回收小程序开发,互联网下发展机遇

在当下生活水平大幅度上升发展下&#xff0c;回收成为了人们日常生活中的一部分。 如今&#xff0c;随着互联网的快速发展&#xff0c;回收行业也进行了升级换代&#xff0c;由传统的线下回收门店到回收箱在到当下的线上互联网回收模式&#xff0c;迈向了“互联网废品回收”的…

如何清除谷歌浏览器的缓存?这里有详细步骤

如果你想解决加载或格式化问题&#xff0c;以改善你在谷歌Chrome上的浏览体验&#xff0c;那么清除缓存和cookie是一个很好的开始。以下是删除它们的方式和操作。 删除缓存和cookie时会发生什么 当你访问一个网站时&#xff0c;它有时会保存&#xff08;或记住&#xff09;某…

使用 Chainlit, Langchain 及 Elasticsearch 轻松实现对 PDF 文件的查询

在我之前的文章 “Elasticsearch&#xff1a;与多个 PDF 聊天 | LangChain Python 应用教程&#xff08;免费 LLMs 和嵌入&#xff09;” 里&#xff0c;我详述如何使用 Streamlit&#xff0c;Langchain, Elasticsearch 及 OpenAI 来针对 PDF 进行聊天。在今天的文章中&#xf…

002 GIS数据的基本格式

1 地理空间信息 地理空间信息的数据模型是现实世界的特征组到理想状态的简化或抽象&#xff0c; 并且可以在各种GIS软件的用户使用层&#xff08;结构化&#xff09;模型有很多。 该层模型由多个空间数据的分层构建&#xff0c;如 图 1.5 。 根据内容&#xff0c;离散特征信息…

React环境配置

1.安装Node.js Node.js官网&#xff1a;https://nodejs.org/en/ 下载之后按默认选项安装好 重启电脑即可自动完成配置 2.安装React 国内使用 npm 速度很慢&#xff0c;可以使用淘宝定制的 cnpm (gzip 压缩支持) 命令行工具代替默认的 npm。 ①使用 winR 输入 cmd 打开终端 ②依…

怎样让MCU/SFU视频会议ovmedia 接入GB28281监控视频参会互动

在国内视频应用对GB监控接入是常规操作&#xff0c;很多系统需要接入监控视频交互处理。我们以ovmedia视频会议为例做一个接入互动。 GB28181协议在流媒体系统较为普及&#xff0c;我们以开源SRS系统对接监控端再接入会议&#xff08;也可以用商用GB流平台&#xff0c;操作基本…

VNCTF 2024 Web方向 WP

Checkin 题目描述&#xff1a;Welcome to VNCTF 2024~ long time no see. 开题&#xff0c;是前端小游戏 源码里面发现一个16进制编码字符串 解码后是flag CutePath 题目描述&#xff1a;源自一次现实渗透 开题 当前页面没啥好看的&#xff0c;先爆破密码登录试试。爆破无果…

MATLAB知识点:uniquetol函数(★★☆☆☆)考虑了一定的容差的unique函数

讲解视频&#xff1a;可以在bilibili搜索《MATLAB教程新手入门篇——数学建模清风主讲》。​ MATLAB教程新手入门篇&#xff08;数学建模清风主讲&#xff0c;适合零基础同学观看&#xff09;_哔哩哔哩_bilibili 节选自第3章&#xff1a;课后习题讲解中拓展的函数 在讲解第三…

wayland(xdg_wm_base) + egl + opengles——dma_buf 作为纹理数据源(五)

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、EGL dma_buf import 相关的数据结构和函数1. EGLImageKHR2. eglCreateImageKHR()3. glEGLImageTargetTexture2DOES()二、egl 中 import dma_buf 作为纹理的代码实例1. egl_wayland_dmabuf_…

洛谷: P1479 宿舍里的故事之五子棋

题目链接: https://www.luogu.com.cn/problem/P1479 思路: 这道题目可以打表或者搜索。每个位置有选择/不选择两种情况。搜索的时候我们一行一行的搜索&#xff0c;直到使用的棋子达到n为止。b[i]为五子连线的数量&#xff0c;b[i] 1表示五子连线的数量可以取i&#xff0c;在…