Java多线程与高并发专题——阻塞和非阻塞队列的并发安全原理是什么?

引入

之前我们探究了常见的阻塞队列的特点,在本文我们就以 ArrayBlockingQueue 为例,首先分析
BlockingQueue ,也就是阻塞队列的线程安全原理,然后再看看它的兄弟——非阻塞队列的并发安全原理。

ArrayBlockingQueue 源码分析

我们首先看一下 ArrayBlockingQueue 的源码,ArrayBlockingQueue 有以下几个重要的属性:

    /*** 用于存储队列元素的数组。该数组是固定大小的,一旦创建,其容量就不能再改变。* 数组中的元素类型为 Object,因为队列可以存储任意类型的元素。*/final Object[] items;/*** 下一次执行 take、poll、peek 或 remove 操作时,从数组中获取元素的索引位置。* 这是一个循环数组,当 takeIndex 达到数组的末尾时,会重新回到数组的起始位置。*/int takeIndex;/*** 下一次执行 put、offer 或 add 操作时,将元素插入到数组中的索引位置。* 同样,这是一个循环数组,当 putIndex 达到数组的末尾时,会重新回到数组的起始位置。*/int putIndex;/*** 当前队列中元素的数量。* 该值始终小于或等于数组的容量,用于跟踪队列中实际存储的元素数量。*/int count;

第一个就是最核心的、用于存储元素的 Object 类型的数组;然后它还会有两个位置变量,分别是takeIndex 和 putIndex,这两个变量就是用来标明下一次读取和写入位置的;另外还有一个 count 用来计数,它所记录的就是队列中的元素个数。

另外,我们再来看下面这三个变量:

    /*** 主锁,用于保护对队列的所有访问操作。* 所有对队列的读写操作都需要先获取这个锁,以确保线程安全。*/final ReentrantLock lock;/*** 用于等待 take 操作的条件对象。* 当队列中没有元素时,尝试从队列中取元素的线程会在此条件上等待。* 当有新元素被添加到队列中时,会通过这个条件唤醒等待的线程。*/private final Condition notEmpty;/*** 用于等待 put 操作的条件对象。* 当队列已满时,尝试向队列中添加元素的线程会在此条件上等待。* 当有元素从队列中被移除时,会通过这个条件唤醒等待的线程。*/private final Condition notFull;

这三个变量也非常关键,第一个就是一个 ReentrantLock,而下面两个 Condition 分别是由ReentrantLock 产生出来的,这三个变量就是我们实现线程安全最核心的工具。

ArrayBlockingQueue 实现并发同步的原理就是利用 ReentrantLock 和它的两个 Condition,读操作和写操作都需要先获取到 ReentrantLock 独占锁才能进行下一步操作。进行读操作时如果队列为空,线程就会进入到读线程专属的 notEmpty 的 Condition 的队列中去排队,等待写线程写入新的元素;同理,如果队列已满,这个时候写操作的线程会进入到写线程专属的 notFull 队列中去排队,等待读线程将队列元素移除并腾出空间。

下面,我们来分析一下最重要的 put 方法:

    /*** 将指定元素插入此队列的尾部,如果队列已满,则等待空间可用。** @param e 要插入的元素* @throws InterruptedException 如果在等待过程中当前线程被中断* @throws NullPointerException 如果指定的元素为 null*/public void put(E e) throws InterruptedException {// 检查传入的元素是否为 null,若为 null 则抛出空指针异常checkNotNull(e);// 获取用于控制队列访问的可重入锁final ReentrantLock lock = this.lock;// 以可中断的方式获取锁,允许线程在等待锁的过程中被中断lock.lockInterruptibly();try {// 当队列中的元素数量达到数组容量时,线程进入等待状态// 等待其他线程从队列中取出元素,释放空间while (count == items.length)notFull.await();// 当队列有空间时,将元素插入队列尾部enqueue(e);} finally {// 无论插入操作是否成功,最后都要释放锁lock.unlock();}}

在 put 方法中,首先用 checkNotNull 方法去检查插入的元素是不是 null。如果不是 null,我们会用ReentrantLock 上锁,并且上锁方法是 lock.lockInterruptibly()。在获取锁的同时是可以响应中断的,这也正是我们的阻塞队列在调用 put 方法时,在尝试获取锁但还没拿到锁的期间可以响应中断的底层原因。

紧接着 ,是一个非常经典的 try  finally 代码块,finally 中会去解锁,try 中会有一个 while 循环,它会检查当前队列是不是已经满了,也就是 count 是否等于数组的长度。如果等于就代表已经满了,于是我们便会进行等待,直到有空余的时候,我们才会执行下一步操作,调用 enqueue 方法让元素进入队列,最后用 unlock 方法解锁。

和 ArrayBlockingQueue 类似,其他各种阻塞队列如 LinkedBlockingQueue、PriorityBlockingQueue、DelayQueue、DelayedWorkQueue 等一系列 BlockingQueue 的内部也是利用了 ReentrantLock 来保证线程安全,只不过细节有差异,比如 LinkedBlockingQueue 的内部有两把锁,分别锁住队列的头和尾,比共用同一把锁的效率更高,不过总体思想都是类似的。

非阻塞队列ConcurrentLinkedQueue

看完阻塞队列之后,我们就来看看非阻塞队列 ConcurrentLinkedQueue。

我们先看看它的源码注释:

An unbounded thread-safe queue based on linked nodes. This queue orders elements FIFO (first-in-first-out). The head of the queue is that element that has been on the queue the longest time. The tail of the queue is that element that has been on the queue the shortest time. New elements are inserted at the tail of the queue, and the queue retrieval operations obtain elements at the head of the queue. A ConcurrentLinkedQueue is an appropriate choice when many threads will share access to a common collection. Like most other concurrent collection implementations, this class does not permit the use of null elements.
This implementation employs an efficient non-blocking algorithm based on one described in Simple, Fast, and Practical Non-Blocking and Blocking Concurrent Queue Algorithms   by Maged M. Michael and Michael L. Scott.
Iterators are weakly consistent, returning elements reflecting the state of the queue at some point at or since the creation of the iterator. They do not throw java. util. ConcurrentModificationException, and may proceed concurrently with other operations. Elements contained in the queue since the creation of the iterator will be returned exactly once.
Beware that, unlike in most collections, the size method is NOT a constant-time operation. Because of the asynchronous nature of these queues, determining the current number of elements requires a traversal of the elements, and so may report inaccurate results if this collection is modified during traversal. Additionally, the bulk operations addAll, removeAll, retainAll, containsAll, equals, and toArray are not guaranteed to be performed atomically. For example, an iterator operating concurrently with an addAll operation might view only some of the added elements.
This class and its iterator implement all of the optional methods of the Queue and Iterator interfaces.
Memory consistency effects: As with other concurrent collections, actions in a thread prior to placing an object into a ConcurrentLinkedQueue happen-before actions subsequent to the access or removal of that element from the ConcurrentLinkedQueue in another thread.
This class is a member of the Java Collections Framework.

翻译:

这是一个基于链表节点的无界线程安全队列。该队列按照先进先出(FIFO)的顺序对元素进行排序。队列头部的元素是在队列中存在时间最长的元素,队列尾部的元素是在队列中存在时间最短的元素。新元素会被插入到队列尾部,而队列的检索操作则从队列头部获取元素。当有多个线程需要共享访问一个公共集合时,ConcurrentLinkedQueue 是一个合适的选择。和大多数其他并发集合实现一样,此类不允许使用 null 元素。
此实现采用了一种高效的非阻塞算法,该算法基于 Maged M. Michael 和 Michael L. Scott 所著的《Simple, Fast, and Practical Non-Blocking and Blocking Concurrent Queue Algorithms》(简单、快速且实用的非阻塞和阻塞并发队列算法)中描述的算法。
迭代器具有弱一致性,它返回的元素反映了迭代器创建时或创建之后某个时刻队列的状态。迭代器不会抛出 java.util.ConcurrentModificationException 异常,并且可以与其他操作并发进行。自迭代器创建以来,队列中包含的元素将恰好被返回一次。
请注意,与大多数集合不同,size 方法并非常量时间操作。由于这些队列具有异步特性,确定当前元素的数量需要遍历元素,因此如果在遍历过程中集合被修改,可能会报告不准确的结果。此外,批量操作(如 addAll、removeAll、retainAll、containsAll、equals 和 toArray)不能保证以原子方式执行。例如,与 addAll 操作并发执行的迭代器可能只会看到部分被添加的元素。
此类及其迭代器实现了 Queue 接口和 Iterator 接口的所有可选方法。
内存一致性效果:与其他并发集合一样,一个线程将对象放入 ConcurrentLinkedQueue 之前的操作,先行发生于另一个线程从该 ConcurrentLinkedQueue 中访问或移除该元素之后的操作。
此类是 Java 集合框架的成员之一。

顾名思义,ConcurrentLinkedQueue 是使用链表作为其数据结构的,我们来看一下关键方法 offer 的源码:

    /*** 将指定元素插入此队列的尾部。由于队列是无界的,此方法将永远不会返回 {@code false}。** @param e 要插入的元素* @return {@code true}(由 {@link Queue#offer} 指定)* @throws NullPointerException 如果指定的元素为 null*/public boolean offer(E e) {// 检查插入的元素是否为 null,如果为 null 则抛出 NullPointerExceptioncheckNotNull(e);// 创建一个新的节点,用于存储要插入的元素final Node<E> newNode = new Node<E>(e);// 从尾节点开始,尝试将新节点插入到队列中for (Node<E> t = tail, p = t;;) {// 获取当前节点的下一个节点Node<E> q = p.next;// 如果下一个节点为 null,说明当前节点是队列的最后一个节点if (q == null) {// p 是最后一个节点// 尝试使用 CAS 操作将新节点设置为当前节点的下一个节点if (p.casNext(null, newNode)) {// 成功的 CAS 操作是元素 e 成为此队列元素的线性化点,// 也是新节点 newNode 成为“活跃”节点的线性化点// 如果当前节点不是尾节点,尝试更新尾节点if (p != t) // 一次跳跃两个节点// 尝试更新尾节点为新节点,失败也没关系casTail(t, newNode); return true;}// 与其他线程的 CAS 竞争失败;重新读取下一个节点} // 如果当前节点的下一个节点是自身,说明我们已经脱离了链表else if (p == q)// 我们已经脱离了链表。如果尾节点没有改变,// 它也会脱离链表,在这种情况下,我们需要跳转到头节点,// 因为所有活跃节点总是可以从头节点到达。否则,新的尾节点是更好的选择。p = (t != (t = tail)) ? t : head;else// 经过两次跳跃后检查尾节点的更新情况p = (p != t && t != (t = tail)) ? t : q;}}

在这里我们不去一行一行分析具体的内容,而是把目光放到整体的代码结构上,在检查完空判断之后,可以看到它整个是一个大的 for 循环,而且是一个非常明显的死循环。在这个循环中有一个非常亮眼的 p.casNext 方法,这个方法正是利用了 CAS 来操作的,而且这个死循环去配合 CAS 也就是典型的乐观锁的思想。

我们就来看一下 p.casNext 方法的具体实现,其方法代码如下:

        /*** 尝试使用CAS(比较并交换)操作将当前节点的next引用从cmp更新为val。** @param cmp 期望的当前next引用值* @param val 要设置的新的next引用值* @return 如果CAS操作成功,则返回true;否则返回false*/boolean casNext(Node<E> cmp, Node<E> val) {return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);}

可以看出这里运用了 UNSAFE.compareAndSwapObject 方法来完成 CAS 操作,而compareAndSwapObject 是一个 native 方法,最终会利用 CPU 的 CAS 指令保证其不可中断。

可以看出,非阻塞队列 ConcurrentLinkedQueue 使用 CAS 非阻塞算法 + 不停重试,来实现线程安全,适合用在不需要阻塞功能,且并发不是特别剧烈的场景。

总结

我们最后来做一下总结。通过我们对阻塞队列和非阻塞队列的并发安全原理的分析,可以知道,其中阻塞队列最主要是利用了 ReentrantLock 以及它的 Condition 来实现,而非阻塞队列则是利用 CAS 方法实现线程安全。

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

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

相关文章

关于ngx-datatable no data empty message自定义模板解决方案

背景&#xff1a;由于ngx-dataable插件默认没有数据时显示的文案是no data to display&#xff0c;且没有任何样式。这里希望通过自定义模板来实现。但目前github中有一个案例是通过设置代码&#xff1a; https://swimlane.github.io/ngx-datatable/empty** <ngx-datatable…

Matlab 双线性插值(二维)

文章目录 一、简介二、实现代码三、实现效果参考资料一、简介 双线性插值是一种 二维插值方法,用于计算 栅格(Grid) 或 像素点 之间的插值值。它主要用于 图像缩放、旋转、变换 等操作,以在新像素位置估算灰度值或颜色值。 如上图所示,假设存在一个二维离散函数(如图像)…

coding ability 展开第二幕(双指针——巩固篇)超详细!!!!

文章目录 前言有效的三角形个数思路 查找总价格为目标值的两个商品思路 两数之和思路 三数之和思路 四数之和思路 总结 前言 本专栏的上篇&#xff0c;讲述了双指针的一些基础的算法习题 今天我们来学习更进一步的双指针用法吧 其实也是大相径庭&#xff0c;和前面的差不多&…

L1-056 猜数字

L1-056 猜数字 - 团体程序设计天梯赛-练习集 (pintia.cn) 题解 这道题要求&#xff1a;一群人坐在一起&#xff0c;每人猜一个 100 以内的数&#xff0c;谁的数字最接近大家平均数的一半就赢。现在需要编写程序来计算&#xff0c;其中需要存入玩家的名字&#xff08;字符串&a…

处理Java中的异常

处理Java中的异常 在 Java 中&#xff0c;异常处理是通过 try-catch-finally 语句来实现的。Java 提供了一种强大的机制&#xff0c;用于捕捉和处理程序运行中的各种错误和异常。通过这种方式&#xff0c;你可以有效地捕捉到可能导致程序崩溃的错误&#xff0c;并做出相应的处…

一维数组的增删改查:对元素的影响

一维数组的增删改查:对元素的影响(C语言) 在C语言中,一维数组是一种存储一组相同类型元素的数据结构。它在内存中是连续存储的,每个元素都可以通过索引来访问和修改。在这篇博文中,我们将详细探讨一维数组的增、删、改、查操作,并分析它们对数组元素的影响。 1. 一维数…

项目实操分享:一个基于 Flask 的音乐生成系统,能够根据用户指定的参数自动生成 MIDI 音乐并转换为音频文件

在线体验音乐创作&#xff1a;AI Music Creator - AI Music Creator 体验者账号密码admin/admin123 系统架构 1.1 核心组件 MusicGenerator 类 负责音乐生成的核心逻辑 包含 MIDI 生成和音频转换功能 管理音乐参数和音轨生成 FluidSynth 集成 用于 MIDI 到音频的转换 …

关于MCP SSE 服务器的工作原理

模型上下文协议&#xff08;Model Context Protocol&#xff0c;简称MCP&#xff09; 是一种全新的开放协议&#xff0c;专门用于标准化地为大语言模型&#xff08;LLMs&#xff09;提供应用场景和数据背景。 你可以把MCP想象成AI领域的“USB-C接口”&#xff0c;它能让不同的A…

计算机:基于深度学习的Web应用安全漏洞检测与扫描

目录 前言 课题背景和意义 实现技术思路 一、算法理论基础 1.1 网络爬虫 1.2 漏洞检测 二、 数据集 三、实验及结果分析 3.1 实验环境搭建 3.2 模型训练 最后 前言 &#x1f4c5;大四是整个大学期间最忙碌的时光,一边要忙着备考或实习为毕业后面临的就业升学做准备,…

win32汇编环境,网络编程入门之二

;运行效果 ;win32汇编环境,网络编程入门之二 ;本教程在前一教程的基础上,研究一下如何得到服务器的返回的信息 ;正常的逻辑是连接上了,然后我发送什么,它返回什么,但是这有一个很尴尬的问题。 ;就是如何表现出来。因为网络可能有延迟,这个延迟并不确定有多久。 ;而程序是顺…

【高分论文密码】AI大模型和R语言的全类型科研图形绘制,从画图、标注、改图、美化、组合、排序分解科研绘图每个步骤

在科研成果竞争日益激烈的当下&#xff0c;「一图胜千言」已成为高水平SCI期刊的硬性门槛——数据显示很多情况的拒稿与图表质量直接相关。科研人员普遍面临的工具效率低、设计规范缺失、多维数据呈现难等痛点&#xff0c;因此科研绘图已成为成果撰写中的至关重要的一个环节&am…

大语言模型-1.2-大模型技术基础

简介 本博客内容是《大语言模型》一书的读书笔记&#xff0c;该书是中国人民大学高瓴人工智能学院赵鑫教授团队出品&#xff0c;覆盖大语言模型训练与使用的全流程&#xff0c;从预训练到微调与对齐&#xff0c;从使用技术到评测应用&#xff0c;帮助学员全面掌握大语言模型的…

uni-app打包成H5使用相对路径

网上找了一圈&#xff0c;没用&#xff0c;各种试&#xff0c;终于给试出来了&#xff0c;主要是网络上的没有第二步&#xff0c;只有第一步&#xff0c;导致打包之后请求的路径没有带上域名 运行的基础路径设置为./ config.js文件里面的baseUrl路径改成空字符&#xff0c;千万…

Android UI性能优化

Android UI性能优化 一、UI性能优化基础 1.1 UI渲染原理 Android系统的UI渲染是通过一个被称为"UI线程"或"主线程"的单线程模型来完成的。系统会以16ms(约60fps)的固定时间间隔发送VSYNC信号,触发UI的渲染流程。如果一帧的处理时间超过16ms,就会出现丢…

【16】单片机编程核心技巧:移位运算的应用

【16】单片机编程核心技巧&#xff1a;移位运算的应用 七律 移位 左迁乘二寄存移&#xff0c;右徙除二暂寄时。 二进玄机藏位里&#xff0c;一移妙法化玄机。 合璧分疆拼字节&#xff0c;置位清零控毫厘。 速效堪超乘除算&#xff0c;单片机中展神威。 摘要 移位运算是单片…

【Linux内核系列】:文件系统

&#x1f525; 本文专栏&#xff1a;Linux &#x1f338;作者主页&#xff1a;努力努力再努力wz ★★★ 本文前置知识&#xff1a; 文件系统初识 那么在我们此前关于文件的学习中&#xff0c;我们学习的都是进程与打开的文件之间的关系&#xff0c;以及打开的文件如何进行管理…

git commit messege 模板设置 (规范化管理git)

配置方法 git config --global core.editor vim &#xff08;设置 Git 的默认编辑器为 Vim&#xff09;在用户根目录下&#xff08;~&#xff09;&#xff0c;创建一个.git_commit_msg文件&#xff0c;然后把下面的内容拷贝到文件中并保存。 [version][模块][类型]{解决xxx问题…

Python和Docker实现AWS ECR/ECS上全自动容器化部署网站前端

以类似ChatGPT的网站前端界面的HTML页面、CSS样式表和JavaScript脚本为例&#xff0c;用Python代码将整个前端代码文件的目录&#xff0c;其中包括所有创建的前端代码文件用Docker打包成镜像文件&#xff0c;提前检查Docker软件是否已经安装&#xff0c;并如果容器服务不存在&a…

无人机全景应用解析与技术演进趋势

无人机全景应用解析与技术演进趋势 ——从立体安防到万物互联的空中革命 一、现有应用场景全景解析 &#xff08;一&#xff09;公共安全领域 1. 立体安防体系 空中哨兵&#xff1a;搭载 77 GHz 77\text{GHz} 77GHz毫米波雷达&#xff08;探测距离 5 km 5\text{km} 5km&…

ChatGPT4.5详细介绍和API调用详细教程

OpenAI在2月27日发布GPT-4.5的研究预览版——这是迄今为止OpenAI最强大、最出色的聊天模型。GPT-4.5在扩大预训练和微调规模方面迈出了重要的一步。通过扩大无监督学习的规模&#xff0c;GPT-4.5提升了识别内容中的模式、建立内容关联和生成对于内容的见解的能力&#xff0c;但…