当前位置: 首页 > 编程日记 > 正文

【多线程】ConcurrentLinkedQueue 的实现原理

1. 引言

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

2. ConcurrentLinkedQueue 的介绍

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

3. ConcurrentLinkedQueue 的结构

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

(图 1)

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

private transient volatile Node<E> tail = head;

4. 入队列

入队列就是将入队节点添加到队列的尾部。为了方便理解入队时队列的变化,以及 head 节点和 tair 节点的变化,每添加一个节点我就做了一个队列的快照图。

(图二)

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

通过 debug 入队过程并观察 head 节点和 tail 节点的变化,发现入队主要做两件事情,第一是将入队节点设置成当前队列尾节点的下一个节点。第二是更新 tail 节点,如果 tail 节点的 next 节点不为空,则将入队节点设置成 tail 节点,如果 tail 节点的 next 节点为空,则将入队节点设置成 tail 的 next 节点,所以 tail 节点不总是尾节点,理解这一点对于我们研究源码会非常有帮助。

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

public boolean offer(E e) {if (e == null) throw new NullPointerException();// 入队前,创建一个入队节点 Node<E> n = new Node<E>(e);retry:// 死循环,入队不成功反复入队。for (;;) {// 创建一个指向 tail 节点的引用 Node<E> t = tail;//p 用来表示队列的尾节点,默认情况下等于 tail 节点。Node<E> p = t;for (int hops = 0; ; hops++) {// 获得 p 节点的下一个节点。Node<E> next = succ(p);//next 节点不为空,说明 p 不是尾节点,需要更新 p 后在将它指向 next 节点 if (next != null) {// 循环了两次及其以上,并且当前节点还是不等于尾节点 if (hops > HOPS && t != tail)continue retry; p = next;} // 如果 p 是尾节点,则设置 p 节点的 next 节点为入队节点。else if (p.casNext(null, n)) {// 如果 tail 节点有大于等于 1 个 next 节点,则将入队节点设置成 tair 节点,更新失败了也 
没关系,因为失败了表示有其他线程成功更新了 tair 节点。if (hops >= HOPS)casTail(t, n); // 更新 tail 节点,允许失败 return true;  } // p 有 next 节点, 表示 p 的 next 节点是尾节点,则重新设置 p 节点 else {p = succ(p);}}}
}

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

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

final Node<E> succ(Node<E> p) {Node<E> next = p.getNext();return (p == next) ? head : next;}

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

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

public boolean offer(E e) {if (e == null)throw new NullPointerException();Node<E> n = new Node<E>(e);for (;;) {Node<E> t = tail;if (t.casNext(null, n) && casTail(t, n)) {return true;}}}

让 tail 节点永远作为队列的尾节点,这样实现代码量非常少,而且逻辑非常清楚和易懂。但是这么做有个缺点就是每次都需要使用循环 CAS 更新 tail 节点。如果能减少 CAS 更新 tail 节点的次数,就能提高入队的效率,所以 doug lea 使用 hops 变量来控制并减少 tail 节点的更新频率,并不是每次节点入队后都将 tail 节点更新成尾节点,而是当 tail 节点和尾节点的距离大于等于常量 HOPS 的值(默认等于 1)时才更新 tail 节点,tail 和尾节点的距离越长使用 CAS 更新 tail 节点的次数就会越少,但是距离越长带来的负面效果就是每次入队时定位尾节点的时间就越长,因为循环体需要多循环一次来定位出尾节点,但是这样仍然能提高入队的效率,因为从本质上来看它通过增加对 volatile 变量的读操作来减少了对 volatile 变量的写操作,而对 volatile 变量的写操作开销要远远大于读操作,所以入队效率会有所提升。

private static final int HOPS = 1;

还有一点需要注意的是入队方法永远返回 true,所以不要通过返回值判断入队是否成功。

5. 出队列

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

从上图可知,并不是每次出队时都更新 head 节点,当 head 节点里有元素时,直接弹出 head 节点里的元素,而不会更新 head 节点。只有当 head 节点里没有元素时,出队操作才会更新 head 节点。这种做法也是通过 hops 变量来减少使用 CAS 更新 head 节点的消耗,从而提高出队效率。让我们再通过源码来深入分析下出队过程。

public E poll() {Node<E> h = head;// p 表示头节点,需要出队的节点 Node<E> p = h;for (int hops = 0;; hops++) {// 获取 p 节点的元素 E item = p.getItem();// 如果 p 节点的元素不为空,使用 CAS 设置 p 节点引用的元素为 null, 如果成功则返回 p 节点的元素。if (item != null && p.casItem(item, null)) {if (hops >= HOPS) {// 将 p 节点下一个节点设置成 head 节点 Node<E> q = p.getNext();updateHead(h, (q != null) ? q : p);}return item;}// 如果头节点的元素为空或头节点发生了变化,这说明头节点已经被另外一个线程修改了。那么获取 p 节点的下一个节点 Node<> next = succ(p);// 如果 p 的下一个节点也为空,说明这个队列已经空了 if (next == null) {// 更新头节点。updateHead(h, p);break;}// 如果下一个元素不为空,则将头节点的下一个节点设置成头节点 p = next;}return null;
}

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

转自:https://www.infoq.cn/article/ConcurrentLinkedQueue/

转载于:https://www.cnblogs.com/itplay/p/11121638.html

相关文章:

shell 脚本简单入门

好久不写shell脚本&#xff0c;有些生疏。总结下shell的语法&#xff0c;以便后续参考&#xff0c;快速捡起来。 shell 脚本执行的3种方式&#xff1a; 1). ./xx.sh &#xff08;xx.sh 需要有执行权限&#xff09; 2). source xx.sh 3). bash xx.sh 变量定义 var2 //注意&…

ubuntu 在线安装mysql_Ubuntu下安装MySQL5.6

我想我们不应该在安装软件上面耽误太多时间&#xff0c;但是很多时候&#xff0c;我们去被安装挡在了门外&#xff0c;尤其是初次在Linux下。作为一个程序猿&#xff0c;最近决定转战linux&#xff0c;MySQL是必须要有的&#xff0c;讲一下我的安装过程。在Ubuntu下安装MySQL有…

js循环动态绑定带参数函数遇到的问题及解决方案[转]

今天写原生javascript时&#xff0c;想利用绑定事件实现类似jquery中on方法的功能&#xff1a;于是有了for循环里绑定事件&#xff0c;无意中发现定义类能解决好多问题&#xff01; 例如&#xff1a;一个不确定长度的列表&#xff0c;在鼠标经过某一条的时候改变背景 1 2 <…

基于Picture Library创建的图片文档库中的上传多个文件功能(upload multiple files)报错怎么解决?...

复现过程 首先&#xff0c;我创建了一个基于Picture Library的图片文档库&#xff0c;名字是 Pic Lib 创建完毕后&#xff0c;我点击它的Upload 下拉菜单&#xff0c;点击Upload Picture按钮 在弹出的对话框中点击 Upload Multiple Files按钮 结果返回了下面的错误页面 如果查看…

vi 环境,跳转函数定义

1, 安装 sudo apt-get install exuberant-ctags 2. 生成tags ctags -R . 3. 跳转 将光标移到想要跳转的函数或变量 快捷键 " CTRL ] " 4. 回转 回到跳转之前的位置&#xff0c; 只需要通过快捷键“ CTRL T ” 其它更详细&#xff1a; https://www.cnblogs.com/ca…

linux kernel内存回收机制

http://www.wowotech.net/linux_kenrel/233.html无论计算机上有多少内存都是不够的&#xff0c;因而linux kernel需要回收一些很少使用的内存页面来保证系统持续有内存使用。页面回收的方式有页回写、页交换和页丢弃三种方式&#xff1a;如果一个很少使用的页的后备存储器是一个…

Python 学习笔记01

print&#xff1a;直接输出 type&#xff0c;求类型 数据类型&#xff1a;字符串&#xff0c;整型&#xff0c;浮点型&#xff0c;Bool型 note01.py # python learning note 01 print(Hello world!) a 10 print a print type(a) a 1.3 print a,type(a) print a Tr…

vuecli 编译后部署_基于vue-cli 打包时抽离项目相关配置文件详解

前言&#xff1a;当使用vue-cli进行开发时时常需要动态配置一些设置&#xff0c;比如接口的请求地址(axios.defaults.baseURL)&#xff0c;这些设置可能需要在项目编译后再进行设置的&#xff0c;所以在vue-cli里我们需要对这些配置文件进行抽离&#xff0c;不让webpack把配置文…

intel xdk 打ios的ipa包

1、打包 2、点击edit。下载csr文件&#xff0c;然后上传到苹果开发者网址&#xff0c;生成cer文件 上面两步搞完&#xff0c;把最后的按钮设置成"yes" 3、上传配置文件 转载于:https://www.cnblogs.com/linn/p/3844930.html

《C++程序设计POJ》《WEEK7 输入输出和模板》《流操纵算子》《文件读写》《二进制文件读写》...

函数指针&#xff0c;运算符重载 人懂我精&#xff0c;人精我深 用的时候查一查手册 dat 二进制文件 如果不指定文件夹&#xff0c;就是生成在当前文件夹&#xff0c;什么是当前文件夹&#xff1f;可执行文件所在的文件夹 绝对路径 相对路径 文件的读写指针 ifstream ofsteam s…

linux内存管理 之 内存节点和内存分区(Zone)

https://www.cnblogs.com/youngerchina/p/5624516.html Linux支持多种硬件体系结构&#xff0c;因此Linux必须采用通用的方法来描述内存&#xff0c;以方便对内存进行管理。为此&#xff0c;Linux有了内存节点、内存区、页框的概念&#xff0c;这些概念也是一目了然的。 内存节…

BZOJ 3585: mex( 离线 + 线段树 )

离线, 询问排序.先处理出1~i的答案, 这样可以回答左端点为1的询问.完成后就用seq(1)将1到它下一次出现的位置前更新. 不断这样转移就OK了--------------------------------------------------------------------#include<bits/stdc.h>using namespace std;#define M(l, r…

yum安装mysql后密码_Centos7:yum安装MySQL5.7后如何设置root密码

Centos下安装软件的方式很简单&#xff0c;只需要通过yum install xxx命令即可。第一步当然检查是否有mysql的yum源&#xff0c;命令&#xff1a;yumlist|grep mysql-community[主要还是安装开源的社区版]如果没有如图所示的和mysql*相关的数据源&#xff0c;可去官网上下载相关…

iOS开发:使用Block在两个界面之间传值(Block高级用法:Block传值)

使用Block的地方很多&#xff0c;其中传值只是其中的一小部分&#xff0c;下面介绍Block在两个界面之间的传值&#xff1a;先说一下思想&#xff1a;首先&#xff0c;创建两个视图控制器&#xff0c;在第一个视图控制器中创建一个UILabel和一个UIButton&#xff0c;其中UILabel…

Vscode 调试 Flutter 项目

1、Vscode 中打开 flutter 项目进行开发 2、运行 Flutter 项目 flutter run r 键:点击后热加载&#xff0c;也就算是重新加载吧。p 键:显示网格&#xff0c;这个可以很好的掌握布局情况&#xff0c;工作中很有用。 o 键:切换 android 和 ios 的预览模式。q 键:退出调试预览模…

Linux地址映射--线性映射与非线性映射

一&#xff0c;线性映射与非线性映射 1. 内存管理 物理内存管理&#xff1a; Linux内存最小管理单位为页&#xff08;page&#xff09;&#xff0c;通常一页为4K。初始化时&#xff0c;linux会为每个物理内存也建立一个page的管理结构&#xff0c;操作物理内存时实际上就…

第三方消息推送回调Java app消息推送第三方选择

由于最先集成的是极光,因此根据官方给的推送设备区分方式中,选择了使用标签tag来进行区分管理方式,其接口提供了设置和清理标签, 每次设置会覆盖上次的结果,当然这个需要和极光后台进行交互,是异步返回的。5、由于其接口没有使用免费和付费区分,对于接口的访问没有限制,从使用的情况来看,经常会出现不准的情况,并且设置标签的效果其实是添加,导致业务需要改变标签时,需要先清除在设置,然而接口又经常出问题,导致这部分也是一塌糊涂了;如果想使用不受免费版本限制特性的推送服务,可以联系平台提供的商务对接,购买付费版本。

[SDOI2015]权值

问题描述&#xff1a; 有一个长度为n的实数序列&#xff0c;,下标从1开始&#xff0c;其中第k个位置的实数为p (sin(a k b) cos(c k d) 2)&#xff0c;sin和cos采用弧度制&#xff0c;其中p&#xff0c;a&#xff0c;b&#xff0c;c&#xff0c;d均为给定的整数。你需要…

为什么前后端都需要进行数据校验?

前端和后端各自的数据完整性校验是相辅相成的。前端校验可以提供即时反馈和优化用户体验,减轻后端服务器压力;后端校验是最终的安全防线,确保数据的完整性和一致性。通过前后端的数据完整性校验机制的结合,可以提供更可靠和安全的应用程序。

多个网站共享一个mysql数据库_如何在多个Postgresql数据库之间共享表

是的,模式是解决方案.使用单个Postgresql集群,使用单个数据库.为所有应用用户创建一个组&#xff1a;CREATE ROLE app;创建全局“应用程序”模式,其中所有全局共享应用程序表都将生效.CREATE SCHEMA AUTHORIZATION app;CREATE TABLE app.objects ( objectid int PRIMARY KEY );…

solr安装-tomcat+solrCloud构建稳健solr集群

solrCloud的搭建可以有两种方式&#xff1a;使用solr内嵌的jetty来搭建&#xff1b;使用外部web容器tomcat来搭建。对于使用jett来搭建参考solr官方的手册照着做肯定ok&#xff0c;下面我主要讲的是如何使用tomcat来搭建solrCloud。废话不多说&#xff0c;开始我们的工作&#…

[pytorch][stepbystep]在pytorch上实现卷积神经网路(CNN)的裁剪(purning)

利用VGG-16对Dogs-vs-Cats数据集进行训练&#xff0c;裁剪VGG-16可以获得3x的运算加速和4x的模型减小 简介 puring神经网络是一个古老的idea,具体可以追溯到1990年&#xff08;与Yann LeCun的最佳脑损伤[1]工作&#xff09;。这个想法是&#xff0c;在网络中的许多参数中&#…

linux内存布局及页面映射

在Linux系统中&#xff0c;以32bit x86系统来说&#xff0c;进程的4GB内存空间&#xff08;虚拟地址空间&#xff09;被划分成为两个部分 ------用户空间和内核空间&#xff0c;大小分别为0-3G&#xff0c;3-4G。用户进程通常情况下&#xff0c;只能访问用户空间的虚拟地址&…

codeforces Kyoya and Colored Balls

题解见&#xff1a;http://blog.csdn.net/libin56842/article/details/46650209 注意这里的组合数取模~~~ 1 /*Author :usedrose */2 /*Created Time :2015/8/7 13:31:44*/3 /*File Name :2.cpp*/4 #pragma comment(linker, "/STACK:1024000000,1024000000") 5 #inc…

存储mysql数据存在特殊字符时处理_转义 存储数据时特殊符号的处理

function url_base64_encode($str){//将这个方法处理后的数据可以存储&#xff0c;不会有特殊符号if($str"")return "";$codebase64_encode($str);//$codedHQ;$codestr_replace(,"!",$code);//把所用""替换成"!"$codestr_re…

虚拟化中的SR-IOV

虚拟化环境中有很多的硬件加速技术&#xff0c;这些技术标准来源于行业内的领导者或各种组织机构&#xff0c;但是在实际项目落地时又有哪些会被启用呢&#xff1f;哪些启用的功能带来了性能上明显的提升呢&#xff1f;那么这些加速技术如果不痛不痒的话那么它们的存在究竟意义…

查看线程的运行状态

实例说明线程共有六个状态&#xff0c;即新建、运行&#xff08;可运行&#xff09;、阻塞、等待、计时等待和终止。当使用new操作符创建新线程时&#xff0c;线程处于“新建状态”。当调用start方法时&#xff0c;线程处于运行&#xff08;可运行&#xff09;状态。当线程需要…

Linux 的内存管理工具和调优参数

1. free 2. top 3. vmstat 4. slabtop; 5. pmap 6. dmesg 7. /proc/meminfo 8. /proc/sys/vm 目录下的文件 9. sync 10./proc/zoneinfo 11./proc/pagetypeinfo 查看内存工具&#xff1a;1.free free - Display amount of free and used memory in the system rootubuntu:/home/…

java多线程查询_利用Java函数式接口处理多线程查询

Java函数式接口有且只有一个抽象方法的接口被称为函数式接口.FunctionalInterface注解: 该注解可用于一个接口的定义上, 一旦使用该注解来定义接口, 编译器将会强制检查该接口是否确实有且仅有一个抽象方法, 否则将会报错.该注解不是必须的, 只要符合函数式接口的定义,那么这个…

奇妙的算法之LCS妙解

LCS算法妙解 LCS问题简述&#xff1a;最长公共子序列 一个数列 S&#xff0c;如果分别是两个或多个已知数列的子序列&#xff0c;且是所有符合此条件序列中最长的&#xff0c;则S 称为已知序列的最长公共子序列。 LCS问题的分支&#xff1a;最长公共子串与最长公共子序列 子串&…