Java并发编程笔记之ConcurrentLinkedQueue源码探究

JDK 中基于链表的非阻塞无界队列 ConcurrentLinkedQueue 原理剖析,ConcurrentLinkedQueue 内部是如何使用 CAS 非阻塞算法来保证多线程下入队出队操作的线程安全?

ConcurrentLinkedQueue是线程安全的无界非阻塞队列,其底层数据结构是使用单向链表实现,入队和出队操作是使用我们经常提到的CAS来保证线程安全的。

我们首先看一下ConcurrentLinkedQueue的类图结构先,好有一个内部逻辑有一个大概的印象,如下图所示:

可以清楚的看到ConcurrentLinkedQueue内部的队列是使用单向链表方式实现,类中两个volatile 类型的Node 节点分别用来存放队列的首位节点。

首先我们先来看一下ConcurrentLinkedQueue的构造函数,如下:

public ConcurrentLinkedQueue() {head = tail = new Node<E>(null);
}

通过无参构造函数可知默认头尾节点都是指向 item 为 null 的哨兵节点。

Node节点内部则维护一个volatile 修饰的变量item 用来存放节点的值,next用来存放链表的下一个节点,从而链接为一个单向无界链表,这就是单向无界的根本原因。如下图:

 

接下来看ConcurrentLinkedQueue 主要关注入队,出队,获取队列元素的方法的源码,如下所示:

1.首先看入队方法offer,offer 操作是在队列末尾添加一个元素,如果传递的参数是 null 则抛出 NPE 异常,否者由于 ConcurrentLinkedQueue 是无界队列该方法一直会返回 true。另外由于使用 CAS 无阻塞算法,该方法不会阻塞调用线程,其源码如下:

 

public boolean offer(E e) {//(1)e为null则抛出空指针异常
    checkNotNull(e);//(2)构造Node节点final Node<E> newNode = new Node<E>(e);//(3)从尾节点进行插入for (Node<E> t = tail, p = t;;) {Node<E> q = p.next;//(4)如果q==null说明p是尾节点,则执行插入if (q == null) {//(5)使用CAS设置p节点的next节点if (p.casNext(null, newNode)) {//(6)cas成功,则说明新增节点已经被放入链表,然后设置当前尾节点if (p != t)casTail(t, newNode);  // Failure is OK.return true;}}else if (p == q)//(7)//多线程操作时候,由于poll操作移除元素后有可能会把head变为自引用,然后head的next变为新head,所以这里需要//重新找新的head,因为新的head后面的节点才是正常的节点。p = (t != (t = tail)) ? t : head;else//(8) 寻找尾节点p = (p != t && t != (t = tail)) ? t : q;}
}

 类图结构时候谈到构造队列时候参构造函数创建了一个 item 为 null 的哨兵节点,并且 head 和 tail 都是指向这个节点,下面通过图形结合来讲解下 offer 操作的代码实现。

  1.首先看一下,当一个线程调用offer(item)时候情况:首先代码(1)对传参判断空检查,如果为null 则抛出空指针异常,然后代码(2)则使用item作为构造函数参数创建一个新的节点,

代码(3)从队列尾部节点开始循环,目的是从队列尾部添加元素。如下图:

 

上图是执行代码(4)时候队列的情况,这时候节点 p , t ,head ,tail 同时指向了item为null的哨兵节点,由于哨兵节点的next节点为null,所以这里q指向也是null。

代码(4)发现q==null  则执行代码(5),通过CAS原子操作判断p 节点的next节点是否为null,如果为null 则使用节点newNode替换p 的next节点,

然后执行代码(6),由于 p == t ,所以没有设置尾部节点,然后退出offer方法,这时候队列的状态图如下:

 

上面讲解的是一个线程调用offer方法的情况下,如果多个线程同时调用,就会存在多个线程同时执行到代码(5),假设线程A调用offer(item1),

线程B调用offer(item2),线程 A 和线程B同时到 p.casNext(null,newNode)。而CAS的比较并设置操作是原子性的,假设线程A先执行了比较设置操作,

则发现当前P的next节点确实是null ,则会原子性更新next节点为newNode,这时候线程B 也会判断p 的next节点是否为null,结果发现不是null,(因为线程 A 已经设置了 p 的 next 为 newNode)则会跳到代码(3),

然后执行到代码(4)的时候的队列分布图如下:

 根据这个状态图可知线程B会执行代码(8),然后q 赋值给了p,这个时候状态图为:

然后线程B再次跳转到代码(3)执行,当执行到代码(4)时候队列状态图为:

由于这时候q == null ,所以线程B 会执行步骤(5),通过CAS操作判断 当前p的next 节点是否为null ,不是则再次循环后尝试,是则使用newNode替换,假设CAS成功了,那么执行步骤(6),

由于 p != t 所以设置tail节点为newNode ,然后退出offer方法。这时候队列的状态图为:

到现在为止,offer代码在执行路径现在就差步骤(7)还没有执行过,其实这个要在执行poll操作才会出现的,这里先看一下执行poll操作后可能会存在的一种情况,如下图所示:

下面分析下当队列处于这种状态调用offer添加元素代码执行到代码(4)的时候的队列状态图,如下:

由于q节点不为空并且p==q 所以执行代码(7),因为 t == tail所以p 被赋值为head ,然后进入循环,循环后执行到代码(4)的时候的队列状态图,如下:

由于 q ==null,所以执行代码(5),进行CAS操作,如果当前没有其他线程执行offer操作,则CAS操作会成功,p的next节点被设置为新增节点,然后执行代码(6),

由于p != t 所以设置新节点为队列尾节点,现在队列状态图,如下:

在这里的自引用的节点会被垃圾回收掉,可见offer操作里面关键步骤是代码(5)通过原子CAS操作来进行控制同时只有一个线程可以追加元素到队列末尾,进行cas竞争失败的线程,

则会通过循环一次次尝试进行cas操作,知道cas成功才会返回,也就是通过使用无限循环里面不断进行CAS尝试方式来替代阻塞算法挂起调用线程,相比阻塞算法,这是使用CPU资源换取阻塞带来的开销。

 

  2.poll操作,poll 操作是在队列头部获取并且移除一个元素,如果队列为空则返回 null,我们首先看改方法的源码,如下:

public E poll() {//(1) goto标记
    restartFromHead://(2)无限循环for (;;) {for (Node<E> h = head, p = h, q;;) {//(3)保存当前节点值E item = p.item;//(4)当前节点有值则cas变为nullif (item != null && p.casItem(item, null)) {//(5)cas成功标志当前节点以及从链表中移除if (p != h) updateHead(h, ((q = p.next) != null) ? q : p);return item;}//(6)当前队列为空则返回nullelse if ((q = p.next) == null) {updateHead(h, p);return null;}//(7)自引用了,则重新找新的队列头节点else if (p == q)continue restartFromHead;else//(8)p = q;}}}
  final void updateHead(Node<E> h, Node<E> p) {if (h != p && casHead(h, p))h.lazySetNext(h);}

poll操作是从队头获取元素,所以代码(2)内层循环是从head节点开始迭代,代码(3)获取当前队头的节点,当队列一开始为空的时候队列状态为:

由于head 节点指向的item 为null 的哨兵节点,所以会执行到代码(6),假设这个过程没有线程调用offer,则此时q等于null  ,如下图:

所以执行updateHead方法,由于h 等于 p所以没有设置头节点,poll方法直接返回null。

假设执行到代码(6)的时候已经有其他线程调用了offer 方法成功添加了一个元素到队列,这时候q执行的是新增元素的节点,这时候队列状态图为:

所以代码(6)判断结果为false,然后会转向代码(7)执行,而此时p不等于q,所以转向代码(8)执行,执行结果是p指向了节点q,此时的队列状态如下:

然后程序转向代码(3)执行,p现在指向的元素值不为null,则执行p.casItem(item, null) 通过 CAS 操作尝试设置 p 的 item 值为 null,

如果此时没有其他线程进行poll操作,CAS成功则执行代码(5),由于此时 p != h ,所以设置头节点为p,poll然后返回被从队列移除的节点值item。此时队列状态为:

这个状态就是前面提到offer操作的时候,offer代码的执行路径(7)执行的前提状态。

假如现在一个线程调用了poll操作,则在执行代码(4)的时候的队列状态为:

可以看到这时候执行代码(6)返回null。

现在poll的代码还有个分支(7)还没有被执行过,那么什么时候会执行呢?假设线程A执行poll操作的时候,当前的队列状态,如下:

那么执行p.casItem(item, null) 通过 CAS 操作尝试设置 p 的 item 值为 null。

假设 CAS 设置成功则标示该节点从队列中移除了,此时队列状态为:

然后由于p != h,所以会执行updateHead 方法,假如线程A执行updateHead前,另外一个线程B开始poll操作,这时候线程B的p指向head节点,

但是还没有执行到代码(6),这时候队列状态为:

然后线程A执行 updateHead 操作,执行完毕后线程 A 退出,这时候队列状态为:

然后线程B继续执行代码(6)q=p.next由于该节点是自引用节点所以p==q,所以会执行代码(7)跳到外层循环restartFromHead,重新获取当前队列队头 head, 现在状态为:

 

总结:poll元素移除一个 元素的时候,只是简单的使用CAS操作把当前节点的item值设置为null,然后通过重新设置头节点让该元素从队列里面摘除,

被摘除的节点就成了孤立节点,这个节点会被在GC的时候会被回收掉。另外,执行分支中如果发现头节点被修改了要跳到外层循环重新获取新的头节点。

 

  3.peek操作,peek 操作是获取队列头部一个元素(只不获取不移除),如果队列为空则返回 null,其源码如下:

public E peek() {//(1)
    restartFromHead:for (;;) {for (Node<E> h = head, p = h, q;;) {//(2)E item = p.item;//(3)if (item != null || (q = p.next) == null) {updateHead(h, p);return item;}//(4)else if (p == q)continue restartFromHead;else//(5)p = q;}}
}

代码结构与poll操作类似,不同于代码(3)的使用只是少了castItem 操作,其实这很正常,因为peek只是获取队列头元素值,并不清空其值,

根据前面我们知道第一次执行 offer 后 head 指向的是哨兵节点(也就是 item 为 null 的节点),那么第一次peek的时候,代码(3)中会发现item==null,

然后会执行 q = p.next, 这时候 q 节点指向的才是队列里面第一个真正的元素或者如果队列为 null 则 q 指向 null。

 

当队列为空的时候,队列状态图,如下:

这时候执行updateHead 由于 h 节点等于 p 节点所以不进行任何操作,然后 peek 操作会返回 null。

当队列中至少有一个元素的时候(假如只有一个),这时候队列状态为:

这时候执行代码(5)这时候 p 指向了 q 节点,然后执行代码(3)这时候队列状态为:

执行代码(3)发现 item 不为 null,则执行 updateHead 方法,由于 h!=p, 所以设置头结点,设置后队列状态为:

可以看到其实就是剔除了哨兵节点。

 

总结:peek操作代码与poll操作类似,只是前者只获取队列头元素,但是并不从队列里面删除,而后者获取后需要从队列里面删除,另外,在第一次调用peek操作的时候,

会删除哨兵节点,并让队列的head节点指向队列里面第一个元素或者null。

 

  4.size方法,获取当前队列元素个数,在并发环境下不是很有用,因为 CAS 没有加锁所以从调用 size 函数到返回结果期间有可能增删元素,导致统计的元素个数不精确。源码如下:

public int size() {int count = 0;for (Node<E> p = first(); p != null; p = succ(p))if (p.item != null)// 最大返回Integer.MAX_VALUEif (++count == Integer.MAX_VALUE)break;return count;
}
//获取第一个队列元素(哨兵元素不算),没有则为null
Node<E> first() {restartFromHead:for (;;) {for (Node<E> h = head, p = h, q;;) {boolean hasItem = (p.item != null);if (hasItem || (q = p.next) == null) {updateHead(h, p);return hasItem ? p : null;}else if (p == q)continue restartFromHead;elsep = q;}}
}
//获取当前节点的next元素,如果是自引入节点则返回真正头节点
final Node<E> succ(Node<E> p) {Node<E> next = p.next;return (p == next) ? head : next;
}

 

  5.remove方法,如果队列里面存在该元素则删除给元素,如果存在多个则删除第一个,并返回 true,否者返回 false。源码如下:

public boolean remove(Object o) {//查找元素为空,直接返回falseif (o == null) return false;Node<E> pred = null;for (Node<E> p = first(); p != null; p = succ(p)) {E item = p.item;//相等则使用cas值null,同时一个线程成功,失败的线程循环查找队列中其它元素是否有匹配的。if (item != null &&o.equals(item) &&p.casItem(item, null)) {//获取next元素Node<E> next = succ(p);//如果有前驱节点,并且next不为空则链接前驱节点到next,if (pred != null && next != null)pred.casNext(p, next);return true;}pred = p;}return false;
}

 

ConcurrentLinkedQueue 底层使用单向链表数据结构来保存队列元素,每个元素被包装为了一个 Node 节点,队列是靠头尾节点来维护的,创建队列时候头尾节点指向一个 item 为 null 的哨兵节点,

第一次 peek 或者 first 时候会把 head 指向第一个真正的队列元素。由于使用非阻塞 CAS 算法,没有加锁,所以获取 size 的时候有可能进行了 offer,poll 或者 remove 操作,导致获取的元素个数不精确,所以在并发情况下 size 函数不是很有用。

 

  • JDK 中基于链表的非阻塞无界队列 ConcurrentLinkedQueue 原理剖析,ConcurrentLinkedQueue 内部是如何使用 CAS 非阻塞算法来保证多线程下入队出队操作的线程安全?

 

转载于:https://www.cnblogs.com/huangjuncong/p/9196240.html

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

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

相关文章

03 Day Python数据类型

一&#xff1a;什么是数据&#xff1f; x10&#xff0c;10是我们要存储的数据 2 为何数据要分不同的类型 数据是用来表示状态的&#xff0c;不同的状态就应该用不同的类型的数据去表示 3 数据类型 数字 字符串 列表 元组 字典 集合 二&#xff1a;数字int #bit_length() 当十进…

border三角形阴影(不规则图形阴影)和多重边框的制作

前言&#xff1a;这是笔者学习之后自己的理解与整理。如果有错误或者疑问的地方&#xff0c;请大家指正&#xff0c;我会持续更新&#xff01; 1. border的组合写法 border&#xff1a;border-width border-style border-color; border-width&#xff1a;边框宽度&#xff0…

JDK 8 Javadoc调整了方法列表

自开始以来&#xff0c; Javadoc输出基本上是静态HTML&#xff0c;具有导航链接和外观的简单样式表样式。 Java SE 7很长时间以来就看到Javadoc输出默认外观的第一个重大变化 &#xff0c;现在看来JDK 8将在生成的Javadoc输出上引入新的变化。 在本文中&#xff0c;我将通过JDK…

hdp安装 不安装mysql_hdp安装及使用问题汇总(一)

1)安装HDP时&#xff0c;如果打印如下错误信息&#xff1a;[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:579)是由于系统的python版本过高&#xff0c;导致验证ssl失败&#xff0c;将python降级为2.7.5以下或修改每个安装节点的python证书验证配置文件…

PhotoSphereViewer 全景图

1网站地址&#xff1a;http://photo-sphere-viewer.js.org/markers.html#demo 2参数中文地址&#xff1a;https://www.cnblogs.com/big-tree/p/5933437.html 使用方法&#xff1a; /*** Initialize the viewer*/ var PSV new PhotoSphereViewer({ // main configuration panor…

常用Lunix命令

计算机 1.硬件系统 输入单元、输出单元、算术逻辑单元、控制单元、记忆单元 中央处理单元&#xff1a;CPU&#xff08;算术逻辑单元、控制单元&#xff09; 电源、主板、CPU、内存&#xff08;RAM&#xff09;、硬盘、&#xff08;声卡、显卡、网卡&#xff09;&#xff08;集成…

angularJS constant和value

angularJS可以通过constant(name,value)和value(name,value)对于创建服务也是很重要的。 相同点是&#xff1a;都可以接受两个参数&#xff0c;name和value。 区别&#xff1a; 1.constant(name,value)可以将一个已经存在的变量值注册为服务&#xff0c;并将其注入到应用的其他…

让我们编写一个文档样式的Web服务

您可能知道&#xff0c;我们可以使用四种主要的Web服务样式。 它们如下&#xff1a; 文件/文学 包装的文件/文学 RPC /编码 RPC /文字 当然&#xff0c;现在不建议使用RPC /编码样式。 如果您有兴趣&#xff0c;可以在此处找到这篇非常全面的文章&#xff0c;以了解不同的…

linux 进入容器,查看和关闭进程

1&#xff0c;linux 查询容器 ID&#xff1a; docker ps 2&#xff0c;进入容器&#xff08;退出 exec 命令用&#xff1a;[ctrlD] &#xff0c;不会终止容器运行。退出 top 命令&#xff1a;ctrl C&#xff09; docker exec -it c39c9d3898c0 /bin/bash 3&#xff0c;查询进程…

mysql表单查询_MySQL表单集合查询

表单查询简单查询SELECT语句查询所有字段指定所有字段&#xff1a;select 字段名1,字段名2&#xff0c;...from 表名;select * from 表名;查询指定字段select 字段名1,字段名2&#xff0c;...from 表名;按条件查询带关系运算符的查询SELECT 字段名1,字段名2,……FROM 表名WHERE…

解决阿里云OSS跨域问题

解决阿里云OSS跨域问题 现象 本人项目中对阿里云图片请求进行了两次&#xff0c;第一次通过img标签进行&#xff0c;第二次通过异步加载获取。第一次请求到图片&#xff0c;浏览器会进行缓存&#xff0c;随后再进行异步请求&#xff0c;保存跨域失效。 错误信息如下&#xff1a…

css之hover改变子元素和其他元素样式

参考地址&#xff1a;链接 表示下一级元素&#xff0c;>表示子元素 1 <!DOCTYPE html>2 <html>3 <head lang"en">4 <meta charset"UTF-8">5 <title></title>6 </head>7 8 <style>9 #a {co…

将JacpFX客户端与JSR 356 WebSockets一起使用

JSR 356 WebSockets是即将发布的JEE 7版本中令人兴奋的新功能之一&#xff0c;并且在其参考实现中包括Server-和Client API。 这使其非常适合在客户端与JavaFX集成。 JacpFX是JavaFX之上的RCP框架&#xff0c;它使用基于消息的方法与组件进行交互。 这种基于消息的方法使集成We…

nagios check_mysql uptime_nagios使用check_mysql监控mysql

如果没有check_mysql插件&#xff0c;需要安装Mysql数据库1、建立专用数据库&#xff1a; [rootsvr3 ~]#mysql -u root -pEnter password:Welcome to the MySQL monitor. Commands end with ; or \g.Your MySQL connection id is 51910Server version: 5.5.3-m3-log Source di…

《精通Spring4.X企业应用开发实战》读后感第七章(AOP概念)

转载于:https://www.cnblogs.com/Michael2397/p/8068486.html

XHTML与HTML的区别

XHTML的语法较为严谨&#xff0c;拥有一定的规则&#xff0c;如果不遵循规则的话容易出错。但也不必太过担心&#xff0c;因为XHTML的规则并不太难&#xff0c;它和HTML4.01标准没有太多的不同。 需要注意的是以下几点&#xff1a; 1.XHTML标签必须被正确的关闭&#xff0c;即…

EC2上的ElasticSearch不到60秒

好奇地看到所有ElasticSearch轮奸是关于什么的&#xff1f; 想在没有大量肘部油脂的情况下看到它吗&#xff1f; 然后&#xff0c;朋友&#xff0c; 别再犹豫了-不到60秒&#xff0c;我将向您展示如何在AWS AMI上安装ElasticSearch 。 您首先需要一个AWS账户以及一个SSH密钥对…

Material使用04 MdCardModule和MdButtonModule综合运用

设计需求&#xff1a;设计一个登陆页面 1 模块导入 1.1 将MdCardModule和MdButtonModule模块导入到共享模块中 import { NgModule } from angular/core; import { CommonModule } from angular/common; import { MdSidenavModule, MdToolbarModule,MdIconModule,MdButtonModule…

mysql子分区多少层_MYSQL子分区修剪

我有一个MYSQL表与分区的年份和子分区的月份。MYSQL子分区修剪CREATE TABLE ptable (id INT NOT NULL AUTO_INCREMENT,name varchar(100),purchased DATETIME NOT NULL,PRIMARY KEY (id, purchased))PARTITION BY RANGE(YEAR(purchased))SUBPARTITION BY HASH(MONTH(purchased)…

65. Valid Number

Validate if a given string is numeric. Some examples:"0" > true" 0.1 " > true"abc" > false"1 a" > false"2e10" > true 判断字符串是否代表了有效数字。 这道题有点坑&#xff0c;情况比较多…… 1 cl…