J.U.C Review - 阻塞队列原理/源码分析

文章目录

  • 阻塞队列的由来
  • BlockingQueue的操作方法
  • BlockingQueue的实现类
    • ArrayBlockingQueue
    • LinkedBlockingQueue
    • DelayQueue
    • PriorityBlockingQueue
    • SynchronousQueue
  • 阻塞队列原理深入分析
    • 1. 构造器和监视器初始化
    • 2. put操作的实现
    • 3. take操作的实现
    • 4. 注意事项
    • 小结
  • 线程池中的阻塞队列
  • BlockingQueue 的使用场景
    • 生产者-消费者模型

在这里插入图片描述


阻塞队列的由来

假设一种典型场景:有一个生产者不断地生产资源,消费者不断地消费资源,所有的资源被存储在一个共享的缓冲池中。生产者将资源放入缓冲池,消费者从缓冲池取出资源进行消费。这种设计模式被称为生产者-消费者模式

生产者-消费者模式的优势在于:

  • 消除了生产者类与消费者类之间的代码依赖性。
  • 将数据生产与数据使用的过程解耦,有助于简化开发过程,并提升系统的负载能力。

但在实现这一模式时,多个线程对共享变量的操作会引发线程安全问题,如重复消费死锁。尤其是在多个生产者和消费者同时存在的情况下,管理共享资源变得更为复杂。当缓冲池为空时,消费者需要等待;当缓冲池满时,生产者需要等待。为了实现这种等待-唤醒机制,需要手动编写复杂的线程同步代码。

幸运的是,Java 的并发包(java.util.concurrent)中已经为我们提供了一个用于简化这类开发的工具——阻塞队列(BlockingQueue)。使用阻塞队列,开发人员不需要再担心在多线程环境下操作共享变量时的线程安全问题。

阻塞队列是 Java 并发包下的重要数据结构。与普通队列不同,它提供了线程安全的访问方式,是并发包中很多高级同步类的基础。

通常,阻塞队列用于生产者-消费者模式。在这种模式中,生产者将数据添加到队列中,消费者从队列中取出数据。阻塞队列就是用来存放这些数据的容器


BlockingQueue的操作方法

阻塞队列提供了四组方法用于插入、移除和检查元素:

操作类型抛出异常返回特殊值一直阻塞超时退出
插入add(e)offer(e)put(e)offer(e, time, unit)
移除remove()poll()take()poll(time, unit)
检查element()peek()--
  • 抛出异常:当操作无法立即执行时,方法会抛出异常。例如,当阻塞队列已满时,调用add(e)将抛出IllegalStateException("Queue full");当队列为空时,调用remove()将抛出NoSuchElementException
  • 返回特殊值:如果操作无法立即执行,方法返回一个特殊值(如true/false)。
  • 一直阻塞:如果操作无法立即执行,方法会阻塞线程直到操作成功或线程被中断。
  • 超时退出:方法会在给定时间内等待操作成功,如果超时则返回一个特殊值(如true/false)。

注意

  1. 阻塞队列中不允许插入null,否则会抛出NullPointerException
  2. 尽量避免调用remove(o)移除特定对象,这种操作效率较低。

BlockingQueue的实现类

ArrayBlockingQueue

ArrayBlockingQueue 是基于数组结构的有界阻塞队列。它的内部结构为一个定长数组,且一旦初始化,队列的大小不能改变。该类构造函数允许指定是否使用公平锁,默认是非公平锁

public ArrayBlockingQueue(int capacity, boolean fair){// 初始化队列大小和公平性设置lock = new ReentrantLock(fair);// 初始化其他参数
}

LinkedBlockingQueue

LinkedBlockingQueue 是基于链表结构的有界阻塞队列。默认情况下,队列大小为Integer.MAX_VALUE,可以手动指定队列大小。该队列遵循**先进先出(FIFO)**原则。

DelayQueue

DelayQueue 是一个特殊的阻塞队列,队列中的元素只有在指定的延迟时间到期后,才能从队列中获取到。队列中的元素必须实现java.util.concurrent.Delayed接口。DelayQueue 是一个无界队列,插入操作永远不会被阻塞,但获取操作会在没有到期元素时被阻塞。

PriorityBlockingQueue

PriorityBlockingQueue 是基于优先级的无界阻塞队列。队列元素的优先级由构造函数中传入的Comparator对象决定。内部使用非公平锁进行线程同步。

public PriorityBlockingQueue(int initialCapacity,Comparator<? super E> comparator) {this.lock = new ReentrantLock(); // 默认使用非公平锁// 初始化其他参数}

SynchronousQueue

SynchronousQueue 是一个没有内部容量的特殊队列。每个插入操作必须等待一个相应的移除操作,反之亦然。

  • iterator() 永远返回空。
  • peek() 永远返回null
  • put() 阻塞,直到有消费者取走元素。
  • offer() 在插入成功后立即返回true,否则返回false
  • take() 阻塞,直到有元素可取。
  • poll() 在取不到元素时立即返回null
  • isEmpty() 永远返回true

阻塞队列原理深入分析

阻塞队列的原理主要基于和**条件变量(Condition)**来控制线程的等待与唤醒机制,确保多线程环境下的线程安全和资源同步。

通过分析ArrayBlockingQueue的源码,我们可以深入理解阻塞队列的工作原理。

1. 构造器和监视器初始化

public ArrayBlockingQueue(int capacity, boolean fair) {// 初始化队列大小和是否公平锁lock = new ReentrantLock(fair);// 初始化消费者和生产者监视器notEmpty = lock.newCondition();notFull =  lock.newCondition();
}
  • 队列数组(items): 用于存储队列元素。
  • 锁(lock): 控制多线程对队列的并发访问,支持公平锁和非公平锁。
  • 条件变量: notEmpty用于标记和唤醒消费者线程,notFull用于标记和唤醒生产者线程。

2. put操作的实现

public void put(E e) throws InterruptedException {checkNotNull(e);final ReentrantLock lock = this.lock;lock.lockInterruptibly();  // 自旋获取锁try {while (count == items.length) {// 如果队列已满,阻塞当前线程,标记为生产者线程notFull.await();}// 插入元素到队列enqueue(e);} finally {lock.unlock();  // 释放锁}
}

put操作分为以下几步:

  1. 获取锁: 线程尝试获取锁。如果没有获取到,线程将自旋等待。
  2. 判断队列是否已满: 如果队列已满,线程调用notFull.await()方法阻塞自己,等待被消费者线程唤醒。此时线程释放锁。
  3. 插入元素: 当队列未满或被唤醒后再次获取到锁,线程将元素插入队列。
  4. 唤醒消费者线程: 插入元素后,线程调用notEmpty.signal()唤醒一个等待的消费者线程。

3. take操作的实现

public E take() throws InterruptedException {final ReentrantLock lock = this.lock;lock.lockInterruptibly();  // 自旋获取锁try {while (count == 0) {// 如果队列为空,阻塞当前线程,标记为消费者线程notEmpty.await();}// 从队列中取出元素return dequeue();} finally {lock.unlock();  // 释放锁}
}

take操作的流程与put操作类似:

  1. 获取锁: 线程尝试获取锁,失败则自旋等待。
  2. 判断队列是否为空: 如果队列为空,线程调用notEmpty.await()阻塞自己,等待被生产者线程唤醒,线程释放锁。
  3. 取出元素: 当队列非空或线程被唤醒并获取到锁后,线程取出队列中的元素。
  4. 唤醒生产者线程: 取出元素后,线程调用notFull.signal()唤醒一个等待的生产者线程。

4. 注意事项

  1. 锁的竞争: puttake操作必须首先获取到锁,才能进行后续操作。没有获取到锁的线程会自旋等待,避免竞争引发的线程安全问题。
  2. 条件等待与唤醒: 如果队列满或空,线程将被阻塞并释放锁,等待相应的生产者或消费者线程唤醒。
  3. 循环判断(while而非if): 使用while循环判断队列状态,确保被唤醒的线程在条件改变后,仍能正确执行后续操作。即使被唤醒,线程还需重新检查队列状态,防止并发条件下的误操作。

小结

阻塞队列通过ReentrantLockCondition机制,确保了在多线程环境下的安全操作。puttake操作通过相应的条件变量,阻塞等待与唤醒操作,使生产者和消费者线程能够有效地协作,从而实现线程间的同步与资源共享。


线程池中的阻塞队列

Java 线程池(ThreadPoolExecutor)中的任务队列就是使用阻塞队列实现的。

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue) {this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,Executors.defaultThreadFactory(), defaultHandler);
}

使用阻塞队列可以有效地管理任务的执行,实现线程池的负载均衡和任务调度。


BlockingQueue 的使用场景

生产者-消费者模式线程池是阻塞队列的两个经典应用场景。

生产者-消费者模型

以下是一个简单的生产者-消费者模型示例:

public class ProducerConsumerExample {private int queueSize = 10;private ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(queueSize);public static void main(String[] args)  {ProducerConsumerExample example = new ProducerConsumerExample();Producer producer = example.new Producer();Consumer consumer = example.new Consumer();producer.start();consumer.start();}class Consumer extends Thread {@Overridepublic void run() {consume();}private void consume() {while (true) {try {queue.take();System.out.println("取走一个元素,队列剩余:" + queue.size() + " 个元素");} catch (InterruptedException e) {e.printStackTrace();}}}}class Producer extends Thread {@Overridepublic void run() {produce();}private void produce() {while (true) {try {queue.put(1);System.out.println("插入一个元素,队列剩余空间:" + (queueSize - queue.size()));} catch (InterruptedException e) {e.printStackTrace();}}}}
}

输出可能如下:

取走一个元素,队列剩余0个元素
取走一个元素,队列剩余0个元素
插入一个元素,队列剩余空间:9
插入一个元素,队列剩余空间:9

注意:由于put()System.out.println()没有加锁,可能会出现日志输出不一致的情况。

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

qmt量化交易策略小白学习笔记第57期【qmt编程之期权数据--获取指定期权品种的详细信息--内置Python】

qmt编程之获取期权数据 qmt更加详细的教程方法&#xff0c;会持续慢慢梳理。 也可找寻博主的历史文章&#xff0c;搜索关键词查看解决方案 &#xff01; 获取指定期权品种的详细信息 该函数能帮助用户获取指定期权品种的详细信息&#xff0c;如期权代码、市场、涨跌停价、期…

c++返回一个pair类型

前言 Under the new standard we can list initialize the return value. 代码测试 #include<iostream> #include<string> #include<vector>std::pair<std::string, int> process(std::vector<std::string>& v) {if (!v.empty()){return …

窖藏之秘:白酒在窖藏过程中经历了哪些变化?

在中华五千年的文明史中&#xff0c;白酒一直扮演着举足轻重的角色。它不仅是文人墨客笔下的灵感源泉&#xff0c;更是亲朋好友间传递情感的桥梁。在众多白酒品牌中&#xff0c;豪迈白酒&#xff08;HOMANLISM&#xff09;以其不同的酿造工艺和窖藏技艺&#xff0c;成为了酒中翘…

【前端面试】设计循环双端队列javascript

题目 https://leetcode.cn/problems/design-circular-deque/description/ 存储循环队列的向量空间是循环的&#xff0c;用通俗的话来讲&#xff0c;就是我们在做next或者prev操作时&#xff0c;不会发生溢出 取模、或者直接判断是否为0/size返回一个值。 数组实现 用函数来…

Python文件自动分类

假如这样的步骤全部手动做下来耗时是6秒&#xff0c;在文件数量不多的情况下&#xff0c;比如10个文件&#xff0c;总共耗时一分钟其实是能够接受的。 但当文件数量特别多时&#xff0c;或者这个操作特别频繁每天都要做十几二十次时&#xff0c;手动操作就会变得耗时又繁琐…

【Agent】Agent Q: Advanced Reasoning and Learning for Autonomous AI Agents

1、问题背景 传统的训练Agent方法是在静态数据集上进行监督预训练&#xff0c;这种方式对于要求Agent能够自主的在动态环境中可进行复杂决策的能力存在不足。例如&#xff0c;要求Agent在web导航等动态设置中执行复杂决策。 现有的方式是用高质量数据进行微调来增强Agent在动…

SpringBoot3.x+MyBatisPlus+druid多数据源配置

1 引言 本章主要介绍SpringBoot3.x多数据源配置&#xff0c;以及在此基础上配置分页拦截&#xff0c;自动填充功等功能&#xff0c;源码链接在文章最后。下面列出几个重要文件进行介绍。 2 项目结构 整体项目结构如下&#xff0c;主要介绍配置文件和配置类。 3 主要代码 …

Android Telephony总结

1、Telephony 业务介绍 Android telephony涉及较多模块 1.1、STK业务介绍 1.1.1、STK域选 1.1.2、是否支持STK Telephon STK-CSDN博客 1.1.3、STK应用的安装卸载 1.2、SS补充业务 1.3、通话业务 1.3.1、紧急号码 ECC 号码总结_ecc号码-CSDN博客 1.4、SMS 1.4.1 短信发送方式…

Datawhale X 李宏毅苹果书 AI夏令营-深度学习入门task3:实践方法论

在应用机器学习算法时&#xff0c;实践方法论能够帮助我们更好地训练模型。 1.模型偏差 模型偏差可能会影响模型训练。举个例子&#xff0c;假设模型过于简单&#xff0c;即使找到的最好的函数也不能满足需求。这种情况就是想要在大海里面捞针&#xff08;一个损失低的函数&am…

数学建模强化宝典(9)遗传算法

前言 遗传算法&#xff08;Genetic Algorithm, GA&#xff09;是一种模拟达尔文生物进化论的自然选择和遗传学机理的生物进化过程的计算模型&#xff0c;它通过模拟自然进化过程来搜索最优解。遗传算法最早由美国的John Holland于20世纪70年代提出&#xff0c;并逐渐成为解决复…

Spring6学习笔记2:容器IoC

文章目录 3 容器&#xff1a;IoC3.1 IoC容器3.1.2 依赖注入3.1.3 IoC容器在Spring的实现 3.2 基于XML管理Bean3.2.1 搭建子模块spring6-ioc-xml3.2.2 实验一&#xff1a;获取bean①方式一&#xff1a;根据id获取②方式二&#xff1a;根据类型获取③方式三&#xff1a;根据id和类…

探索英文字体设计的奥秘,解读风格与实用技巧

英文字体设计是一门融合了艺术与技术的学科。字体不仅仅是文本的视觉表现&#xff0c;更是传递情感、信息和品牌个性的媒介。从印刷时代到数字时代&#xff0c;英文字体的设计和应用发生了巨大的变化&#xff0c;而现代字体设计师则肩负着为视觉传达赋予新生命的使命。本文将深…

记:子线程实现QTcpSocket读写的问题

最近在改进考勤系统客户端多线程实现时遇到了线程异步和野指针问题 client&#xff1a;多线程实现ui界面显示&#xff08;主线程&#xff09;、人脸检测&#xff08;检测线程&#xff09;、socket网络通信&#xff08;通信线程)三个任务。 主线程&#xff1a; TimerEvent实时…

CTF---密码学知识点总结

✨Ascall编码&#xff1a;在 ctf 比赛中&#xff0c;flag 的标志一般是以 Ascall 码的形式存在&#xff0c;其对应的码值为102&#xff0c;108&#xff0c;97&#xff0c;103&#xff08;其中{的码值是123&#xff09;&#xff01; ✨Unicode编码&#xff1a;又名万国码&#…

kubeadm方式升级k8s集群

一、注意事项 升级前最好备份所有组件及数据&#xff0c;例如etcd 不要跨两个大版本进行升级&#xff0c;可能会存在版本bug&#xff0c;如&#xff1a; 1.19.4–>1.20.4 可以 1.19.4–>1.21.4 不可以 跨多个版本的可以逐个版本进行升级。 二、查看当前版本 [rootk8s…

如何远程连接其他电脑?两种常用方法!

在过去&#xff0c;远程控制仅限于那些擅长计算机专业技术的少数人。然而&#xff0c;随着科学技术的不断发展&#xff0c;越来越多的人可以通过各种远程控制软件实现对其他电脑的远程操作。如今&#xff0c;对于普通电脑用户来说&#xff0c;使用自己的电脑远程控制另一台电脑…

22AP10 SS524 平替 海思HI3521DV200 可提供开发资料

22AP10 是针对多路高清/超高清&#xff08;1080p/4M/5M/4K&#xff09;DVR 产品应用开发的新一代专 业 SoC 芯片。22AP10 集成了 ARM Cortex-A7 四核处理器和性能强大的图像分析工具 推理引擎&#xff0c;支持多种智能算法应用。同时&#xff0c;22AP10 还集成了多路 MIPI …

通过EasyExcel设置自定义表头及设置特定单元格样式、颜色

前言 在项目开发中&#xff0c;我们会遇到各种文件导出的开发场景&#xff0c;但是这种情况并都不常用&#xff0c;于是本人将自己工作中所用的代码封装成工具类&#xff0c;旨在记录工具类使用方法和技术分享。 实战代码 导出效果&#xff1a; 1、导入依赖 <dependency&g…

钢铁百科:A633GrE钢板材质、A633GrE力学性能、A633GrE执行标准

A633GrE钢板是一种美标低合金高强度结构钢板&#xff0c;具有多种优异的性能和应用领域。以下是对其材质、执行标准、化学成分、力学性能、交货状态、应用范围、常用规格及总结的详细介绍&#xff1a; 一、A633GrE材质 A633GrE钢板属于美标低合金钢&#xff0c;具有高强度、高…

Leetcode 第 410 场周赛题解

Leetcode 第 410 场周赛题解 Leetcode 第 410 场周赛题解题目1&#xff1a;3248. 矩阵中的蛇思路代码复杂度分析 题目2&#xff1a;3249. 统计好节点的数目思路代码复杂度分析 题目3&#xff1a;3250. 单调数组对的数目 I思路代码复杂度分析 题目4&#xff1a;3251. 单调数组对…