【JUC】浅析ConcurrentLinkedQueue

2023-10-31

【JUC】浅析ConcurrentLinkedQueue

一、前言

在并发编程中,有时候需要使用线程安全的队列。如果要实现一个线程安全的队列有两种方式:一种是使用阻塞算法,另一种是使用非阻塞算法。使用阻塞算法的队列可以用一个锁(入队和出队用同一把锁)或两个锁(入队和出队用不同的锁)等方式来实现。非阻塞的实现方式则可以使用循环CAS的方式来实现。本节让我们一起来研究一下DougLea是如何使用非阻塞的方式来实现线程安全队列ConcurrentLinkedQueue的,相信从大师身上我们能学到不少并发编程的技巧。

ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部;当我们获取一个元素时,它会返回队列头部的元素。它采用了“wait-free”算法(即CAS算法)来实现,该算法在Michael&Scott算法上进行了一些修改。

二、ConcurrentLinkedQueue的结构

通过ConcurrentLinkedQueue的类图来分析一下它的结构

image-20230504194016384

ConcurrentLinkedQueue由head节点和tail节点组成,每个节点(Node)由节点元素(item)和指向下一个节点(next)的引用组成,节点与节点之间就是通过这个next关联起来,从而组成一张链表结构的队列。默认情况下head节点存储的元素为空,tail节点等于head节点。

image-20230504194048249

三、入队列

3.1、入队列的过程

入队列就是将入队节点添加到队列的尾部。

为了方便理解入队时队列的变化,以及head节点和tail节点的变化,这里以一个示例来展开介绍。假设我们想在一个队列中依次插入4个节点,为了帮助大家理解,每添加一个节点就做了一个队列的快照图。

image-20230504194304259

  • 加元素1。队列更新head节点的next节点为元素1节点。又因为tail节点默认情况下等于head节点,所以它们的next节点都指向元素1节点。
  • 加元素2。队列首先设置元素1节点的next节点为元素2节点,然后更新tail节点指向元素2节点。
  • 加元素3,设置tail节点的next节点为元素3节点。
  • 加元素4,设置元素3的next节点为元素4节点,然后将tail节点指向元素4节点。

通过调试入队过程并观察head节点和tail节点的变化,发现入队主要做两件事情:

  • 第一是将入队节点设置成当前队列尾节点的下一个节点;
  • 第二是更新tail节点,如果tail节点的next节点不为空,则将入队节点设置成tail节点,如果tail节点的next节点为空,则将入队节点设置成tail的next节点,所以tail节点不总是尾节点(理解这一点对于我们研究源码会非常有帮助)。

通过对上面的分析,我们从单线程入队的角度理解了入队过程,但是多个线程同时进行入队的情况就变得更加复杂了,因为可能会出现其他线程插队的情况。如果有一个线程正在入队,那么它必须先获取尾节点,然后设置尾节点的下一个节点为入队节点,但这时可能有另外一个线程插队了,那么队列的尾节点就会发生变化,这时当前线程要暂停入队操作,然后重新获取尾节点。让我们再通过源码来详细分析一下它是如何使用CAS算法来入队的。

image-20230504194714652

从源代码角度来看,整个入队过程主要做两件事情:第一是定位出尾节点;第二是使用CAS算法将入队节点设置成尾节点的next节点,如不成功则重试。

3.2、定位尾节点

tail节点并不总是尾节点,所以每次入队都必须先通过tail节点来找到尾节点。尾节点可能是tail节点,也可能是tail节点的next节点。代码中循环体中的第一个if就是判断tail是否有next节点,有则表示next节点可能是尾节点。获取tail节点的next节点需要注意的是p节点等于p的next节点的情况,只有一种可能就是p节点和p的next节点都等于空,表示这个队列刚初始化,正准备添加节点,所以需要返回head节点。获取p节点的next节点代码如下。

image-20230504195021525

3.3、设置入队节点为尾节点

p.casNext(null,n)方法用于将入队节点设置为当前队列尾节点的next节点,如果p是null,表示p是当前队列的尾节点,如果不为null,表示有其他线程更新了尾节点,则需要重新获取当前队列的尾节点。

3.4、HOPS的设计意图

上面分析过对于先进先出的队列入队所要做的事情是将入队节点设置成尾节点,doug lea写的代码和逻辑还是稍微有点复杂。那么,我用以下方式来实现是否可行?

image-20230504195155563

让tail节点永远作为队列的尾节点,这样实现代码量非常少,而且逻辑清晰和易懂。但是,这么做有个缺点,每次都需要使用循环CAS更新tail节点。

如果能减少CAS更新tail节点的次数,就能提高入队的效率,所以doug lea使用hops变量来控制并减少tail节点的更新频率,并不是每次节点入队后都将tail节点更新成尾节点,而是当tail节点和尾节点的距离大于等于常量HOPS的值(默认等于1)时才更新tail节点,tail和尾节点的距离越长,使用CAS更新tail节点的次数就会越少,但是距离越长带来的负面效果就是每次入队时定位尾节点的时间就越长,因为循环体需要多循环一次来定位出尾节点,但是这样仍然能提高入队的效率。

因为从本质上来看它通过增加对volatile变量的读操作来减少对volatile变量的写操作,而对volatile变量的写操作开销要远远大于读操作,所以入队效率会有所提升。

image-20230504195309790

四、出队列

出队列的就是从队列里返回一个节点元素,并清空该节点对元素的引用。让我们通过每个节点出队的快照来观察一下head节点的变化。

image-20230504195346029

从图中可知,并不是每次出队时都更新head节点,当head节点里有元素时,直接弹出head节点里的元素,而不会更新head节点。只有当head节点里没有元素时,出队操作才会更新head节点。

这种做法也是通过hops变量来减少使用CAS更新head节点的消耗,从而提高出队效率。让我们再通过源码来深入分析下出队过程。

image-20230504195455878

首先获取头节点的元素,然后判断头节点元素是否为空,如果为空,表示另外一个线程已经进行了一次出队操作将该节点的元素取走,如果不为空,则使用CAS的方式将头节点的引用设置成null,如果CAS成功,则直接返回头节点的元素,如果不成功,表示另外一个线程已经进行了一次出队操作更新了head节点,导致元素发生了变化,需要重新获取头节点。

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

【JUC】浅析ConcurrentLinkedQueue 的相关文章

随机推荐

  • 刻章不要钱 5个在线印章制作工具

    俺的博客里的图片 还有网生代上俺写的文章很多都是用印章当作图片水印的 奇怪的是 怎么没人眼馋 有了现代科技 刻章其实很简单了 本文就介绍几个在线印章制作工具 一 MakePic印章生成器 允许输入2 4个汉字 可选择的字体有 经典繁印篆 经
  • 算法导论 学习笔记 第三章 函数的增长

    当输入规模足够大 要研究算法的渐近效率 即我们关心当输入规模无限增加时 在极限中 算法的运行时间如何随着输入规模的变大而增加 主要使用以下渐近记号描述算法的运行时间 1 记号 给定一个函数g n 用 g n 表示以下函数的集合 若存在正常量
  • python之路-untitest单元测试框架组件使用详细介绍

    文章目录 unittest xmind思维导图 UnitTest介绍 TestCase TestSuite TextTestRunner TestLoader TestSuite和TestLoader的使用区别 小结 Fixture 方法级
  • 北京政府占股扶持机构

    1 北京中关村发展集团股份有限公司 2 北京中海投资管理有限公司 http www zhtzgl cn 3 北京首都科技集团有限责任公司 4 亦庄国际 http www etowncapital com zjtz columnsId 40
  • Linux线程知识总结

    1 编程头文件
  • react-实现页面跳转

    Link a标签 需设置path属性 值为路径 点击后会跳转到指定的路径 Router 用来包裹整个组件 Route 指定对应路径所展示的组件 Route写在哪里组件就展示在哪里 路由会给组件提供history属性 在this props里
  • Transformers训练预处理datasets出现Socket Timeout

    原因 ddp的时候默认等待时间是1800s 如果超出这个时间程序就会退出 解决方法 更新transformers库 低版本不支持如下方式 并添加参数 ddp timeout 3600 这里3600s只是demo 具体根据自身程序来设置
  • 手把手前端入门笔记之vue-element-admin-01

    前言 本文主要为vue element admin框架的入门教程 本人2年后端开发经验 想自学前端转全栈工程师 听着就好酷 直接上手实战应该是入门前端最快的方式了 在此记录下学习过程 希望可以对初学者有所帮助 如有错误或未考虑完全的地方 望
  • 以太坊使用puppeth工具

    puppeth源于官方的项目编译后 https github com ethereum go ethereum即可得到要得到的内容 只想用工具 不想自己编译 博主编译了一份所有的工具都有 下载 https download csdn net
  • LeetCode题目笔记——面试题 01.03. URL化

    文章目录 题目描述 题目难度 简单 方法一 替换空格 代码 Python 方法二 构造新字符串 代码 Python C 方法三 将 20插入到原字符串中 总结 题目描述 URL化 编写一种方法 将字符串中的空格全部替换为 20 假定该字符串
  • Weblogic 12c 集群环境搭建

    本文是在windows7操作系统下配置的 jdk版本1 7 weblogic版本12 1 3 0 0 搭建集群前的规划 其中AdminServer是总控制端 server1 server2 server3是集群中的三个服务节点 其中Admi
  • vue2 new Date() 转换为年月日时分秒以及星期几(padStart补零) - 附完整示例

    new Date 效果 2022年07月12日 星期二 17 19 29 一 new Date 在vue2中使用new Date 转换为年月日时分秒以及星期几 padStart补零 二 使用步骤 1 data中声明定时器以及在methods
  • Sketch装机必备!10款Sketch 插件使用率超高!

    本文给大家推荐和整理了 10款 使用率超高的 Sketch 插件 Sketch 是一款深受 UI 设计师欢迎的 UI 设计工具 由于其轻便的格式 简洁的 UI 界面操作 很快风靡 UI 设计行业 其 Sketch 的插件尤为强大 可谓是让
  • matlab练习程序(线性分类器<最小二乘>)

    clear all close all clc num 4 元素数量 k 180 迭代次数 step 0 1 迭代步长 w 1 0 5 1 1 权值 x 1 0 0 输入的值 每行为一组 1 1 0 1 0 1 1 1 1 d 1 0 1
  • 两种产生随机数的方式之间的对比(Math.random()方法 和 Random类)

    在实际开发中 产生随机数的使用很普遍 而在JAVA中主要提供了两种方式产生随机数 其一 调用Math类中的random 方法 其二 使用Random类 一 Math random 方法 基本概述 这个方法能够产生在 0 0 1 0 之间的随
  • VueCli3+vue2.6兼容ie11

    一 首先确定babel polyfill版本号 babel polyfill 7 4 0以前使用babel polyfill 之后使用core js stable 和 regenerator runtime 可参考官方文档babel pol
  • Redis报错 Creating Server TCP listening socket 127.0.0.1:6379: bind: No error

    cmd窗口输入 redis server exe redis windows conf 报错 Creating Server TCP listening socket 127 0 0 1 6379 bind No error 解决方法 cm
  • 【毕业设计】图像处理-毕业设计-相关题目-含matlab源代码

    毕业设计 0001 基于图像处理的一维条形码识别 含MATLAB源码 毕业设计 0002 一种图像中值滤波 边缘检测 hough变换检测直线的用户界面开发 含matlab源代码 如有疑问 可私信博主 持续更新中
  • java文档注释的基本认识

    Java 程序员都应该知道 JDK 开发最好的帮助信息就来自 SUN 发布的 Java 文档 它分包分类地提供了各方法 属性的帮助信息 具有详细的类树信息 索引信息等 并提供了许多相关类之间的关系 如继承 实现接口 引用等 Java 文档全
  • 【JUC】浅析ConcurrentLinkedQueue

    JUC 浅析ConcurrentLinkedQueue 文章目录 JUC 浅析ConcurrentLinkedQueue 一 前言 二 ConcurrentLinkedQueue的结构 三 入队列 3 1 入队列的过程 3 2 定位尾节点