热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

ConcurrentLinkedQueue深度源码剖析

在Java的并发包中,存在着许多高效的并发工具类,它优于synchronized关键字,在JDK中提供了一个ConcurrentLinkedQueue工具类实现了高效的并发读写工具

在Java的并发包中,存在着许多高效的并发工具类,它优于synchronized关键字,在JDK中提供了一个ConcurrentLinkedQueue工具类实现了高效的并发读写工具类,该工具类具有很高效的性能,因此,本片文章笔者将通过解读ConcurrentLinkedQueue源码的方式探究该数据结构的内部构造。

一、无锁(CAS算法)
在介绍这个工具类之前,先来讲讲无锁的概念以及其算法实现,因为在JDK的并发包中的工具类之所以有高效的性能,大多来源于采用了无锁的算法。因此必须掌握无锁的原理才能理解并发工具类的实现原理。

那么?什么是无锁呢?为什么无锁的性能更佳呢?我们都知道,在并发的环境下,要保证数据的一致性,多个线程共享同一个资源都必须实现同步,否则会产生严重的后果,我们首先想到采用synchronized来实现同步访问资源,没错,这种方式是传统的并发环境下的解决方案,可是,使用该关键字同步访问也就意味着阻塞访问,当在高并发的生产环境中,一个线程占用了共享资源,其他线程就将等待该线程释放资源,对于系统的性能影响非常严重。因此,提出了无锁的概念。无锁是一种非阻塞的方式,因此它在并发环境下不会发生死锁,也就是说它避免了之前阻塞方式下锁的资源竞争给系统带来的开销,也避免了线程之前频繁挂起,唤醒的调度开销,因此,它的性能比基于锁的方式更加优越。但是,性能提高了,它的内部员原理实现也比较复杂,当然,为了提高性能这是有必要的。

无锁算法CAS(全称CompareAndSwap),该算法包含三个过程CAS(V,E,N),V表示当前值,E表示期望值,N表示更新值,该算法的实现是这样的:当V当前值等于期望值E的时候,就将V更新为N,如果不相等,表示有其他线程修改过改值,因此,不做任何操作,在该算法内部,实现了一个死循环,因此在当有其他线程修改过当前值时,当前线程就会发现,并不做任何操作,再次循环,一直尝试,知道修改成功。之前看到有人提到这样一个问题:如果在判断当前值和期望值是否相等与设置新值之间有其他线程修改了当前值该怎么办,这样不是产生了脏数据吗?其实在CAS操作在硬件层面上,已经实现了原子化的CAS指令,不会发生这种问题。这里就不深究了,读者可自行去查看源码探究。

在介绍完无锁的概念后,我们来进入正题,探究ConcurrentLinkedQueue的内部原理。

二、剖析ConcurrentLinkedQueue
ConcurrentLinkedQueue的性能确实非常的高,但是它的内部实现相当的复杂。它是一个链表的数据结构,实现了Queue接口。它内部实现了一个静态内部类定义了它的节点变量。

上图是这个内部类的成员变量,item用来表示当前元素,next执行下一个元素。这个很好理解。在这个内部类中使用了CAS操作,也就是说在并发环境下是线程安全的。

casItem(E cmp, E val)表示使用CAS操作设置当前值,casNext(Node cmp, Node val)设置下一个节点。
在ConcurrentLinkedQueue的内部,有两个重要的成员变量head,tail,head指向链表的表头,tail指向链表的表尾,在它的无参构造器中,head和tail指向了一个空节点,表示一个空链表。

它也给出了一个带参的构造器,允许传入一个集合。

这个构造器主要是将Collection结合中的元素建立一个链表如果该集合为空,则会抛出空指针异常。
在add方法中调用了一个offer方法,这个方法是ConcurrentLinkedQueue内部实现原理的一个重点,也是一个难点,比较复杂。

下面,我们来看看offer方法的具体实现:

public boolean offer(E e) {
checkNotNull(e);
final Node newNode = new Node(e);

for (Node t = tail, p = t;;) {
Node q = p.next;
if (q == null) {
// p是最后一个节点
if (p.casNext(null, newNode)) {
//每两次更新一下tail
if (p != t)
casTail(t, newNode); // Failure is OK.
return true;
}
// CAS操作竞争失败,再次尝试
}
else if (p == q)
//遇到哨兵节点,从head开始遍历,但是如果tail被修改,则使用tail
//因为它有可能被其他线程修改成功,因此,该操作在并发环境下才会返回true
p = (t != (t = tail)) ? t : head;
else
//取下一个节点或者最后一个节点
p = (p != t && t != (t = tail)) ? t : q;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
该方法首先会检查加入的元素是否为空,如果为空会抛出一个空指针异常,如果不为空,实例化一个节点,进入for循环,该循环是整个方法的核心,由CAS操作完成,是一个死循环,没有出口,一直尝试,直到成功返回。在刚开始,整个链表是空的,head和tail是空的,所以在for循环内部p,t指向头一个空节点,p.next当然为空,因此q也指向一个null值,进入if循环,因为当前值p为null满足期望值,因此进入if语句if (p.casNext(null, newNode)),但是p==t,所以跳过casTail(t, newNode)语句,表示tail没有更新,因此在casNext成功的时候,返回true表示加入元素成功,如果不成功则不返回一直进行尝试直到成功。当加入第二个元素的时候,p.next指向第一个增加的元素,q!=null表示q不是最后一个节点,但是我们要插入一个元素必须找到最后一个节点才能进行插入,因此程序进入 p = (p != t && t != (t = tail)) ? t : q;语句查找尾节点。这个语句在第一个节节点加入成功后进入第二值的时候p != t则会返回q,而q表示p.next,所以该语句这时相当于p=p.next指向下一个节点,也就是尾节点。这时再次循环进入if(q == null)语句,此时由于t在插入第一个节点是没有更新,而p已经更新为尾节点,因此if (p != t)成立,执行casTail方法更新tail为尾节点,返回true表示第二个节点插入成功。由此可以看出,tail每插入两个节点才更新一次。

在上面的源码中,有p==q的情况,这种情况是由于遇到了哨兵,什么是哨兵呢?所谓哨兵节点就是next执行了自己的节点,因为在for循环刚开始 Node q = p.next;如果该if语句成立,表示自己指向了自己,next无法指向了下一个节点,因此我们需要寻找下一个节点来找到尾节点,p = (t != (t = tail)) ? t : head;语句在极大多数情况下会返回head,也就是找不到下一个节点,只能从头开始遍历,在单线程下只能总是从头找起,但是在高并发环境下,t有可能会被其他线程修改,因此t!=t成立,返回tail,这里,有的读者可能不会太明白,t!=t怎么会成立呢?这里解释一下:因为!=操作符不是原子操作符,在该操作符之前我们拿到了t的值,也许在拿到该值之后要去进行比较的时候另外的线程修改了t的值,这样就造成了t!=t成立。所以它本着一种乐观的态度去尝试,也许会有其他线程将t修改成功,此时就可以找到尾节点,不必再从头找起,浪费时间,写到这里笔者深切的体会到该算法的高明之处,极其高明。

对于poll方法,删除一个节点,和上面的offer方法的算法一样,head也是执行两次之后才会更新。有兴趣的读者可以自己查看源码探究。


下面再简单看一下remove方法,删除指定元素节点:

remove方法,它会从头开始遍历,判断是否与指定删除的元素相等,如果不相等,指向下一个节点,succ方法返回下一个节点元素,然后进入下一个循环,如果执行removed = p.casItem(item, null)语句表示找到了要删除的元素,然后将该元素所在的节点值设置为null,如果设置成功则返回true,然后找到下一个节点next,在判断了当前节点与next不为空之后就将当前节点的next指向要删除节点的下一个节点,达到删除的目的,最后返回true表示删除成功。该方法比较简单,只要熟悉 链表的删除等操作不难理解该方法的实现。

该类中还有很多方法,这里不一一讲解了,关键只要理解了它的设置原理,读懂一个方法,其他的也就类似了,有兴趣的读者可以参考JDK源码查看。


最后,要说的是,通过上面可以看出大量的使用了CAS操作,因此要掌握这些并发工具类,理解CAS算法是关键,不过使用了该算法也加大了程序设计的难度,但是对于性能的高速提升,我们认为该设计是完全有必要的。

至此,就介绍完该并发工具类的内部原理,笔者的能力有限,如有不足之处可以指出共同探讨,谢谢!!!
————————————————
版权声明:本文为CSDN博主「不清不慎」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_37142346/article/details/80057713

ConcurrentLinkedQueue深度源码剖析



推荐阅读
  • 微软推出Windows Terminal Preview v0.10
    微软近期发布了Windows Terminal Preview v0.10,用户可以在微软商店或GitHub上获取这一更新。该版本在2月份发布的v0.9基础上,新增了鼠标输入和复制Pane等功能。 ... [详细]
  • 解决Bootstrap DataTable Ajax请求重复问题
    在最近的一个项目中,我们使用了JQuery DataTable进行数据展示,虽然使用起来非常方便,但在测试过程中发现了一个问题:当查询条件改变时,有时查询结果的数据不正确。通过FireBug调试发现,点击搜索按钮时,会发送两次Ajax请求,一次是原条件的请求,一次是新条件的请求。 ... [详细]
  • 第二十五天接口、多态
    1.java是面向对象的语言。设计模式:接口接口类是从java里衍生出来的,不是python原生支持的主要用于继承里多继承抽象类是python原生支持的主要用于继承里的单继承但是接 ... [详细]
  • 本文详细解析了Autofac在高级应用场景中的具体实现,特别是如何通过注册泛型接口的类来优化依赖注入。示例代码展示了如何使用 `builder.RegisterAssemblyTypes` 方法,结合 `typeof(IEventHandler).Assembly` 和 `Where` 过滤条件,动态注册所有符合条件的类,从而简化配置并提高代码的可维护性。此外,文章还探讨了这一方法在复杂系统中的实际应用及其优势。 ... [详细]
  • 本指南详细介绍了如何利用华为云对象存储服务构建视频点播(VoD)平台。通过结合开源技术如Ceph、WordPress、PHP和Nginx,用户可以高效地实现数据存储、内容管理和网站搭建。主要内容涵盖华为云对象存储系统的配置步骤、性能优化及安全设置,为开发者提供全面的技术支持。 ... [详细]
  • 在分析和解决 Keepalived VIP 漂移故障的过程中,我们发现主备节点配置如下:主节点 IP 为 172.16.30.31,备份节点 IP 为 172.16.30.32,虚拟 IP 为 172.16.30.10。故障表现为监控系统显示 Keepalived 主节点状态异常,导致 VIP 漂移到备份节点。通过详细检查配置文件和日志,我们发现主节点上的 Keepalived 进程未能正常运行,最终通过优化配置和重启服务解决了该问题。此外,我们还增加了健康检查机制,以提高系统的稳定性和可靠性。 ... [详细]
  • 深入解析CAS机制:全面替代传统锁的底层原理与应用
    本文深入探讨了CAS(Compare-and-Swap)机制,分析了其作为传统锁的替代方案在并发控制中的优势与原理。CAS通过原子操作确保数据的一致性,避免了传统锁带来的性能瓶颈和死锁问题。文章详细解析了CAS的工作机制,并结合实际应用场景,展示了其在高并发环境下的高效性和可靠性。 ... [详细]
  • POJ 2482 星空中的星星:利用线段树与扫描线算法解决
    在《POJ 2482 星空中的星星》问题中,通过运用线段树和扫描线算法,可以高效地解决星星在窗口内的计数问题。该方法不仅能够快速处理大规模数据,还能确保时间复杂度的最优性,适用于各种复杂的星空模拟场景。 ... [详细]
  • 装饰者模式(Decorator):一种灵活的对象结构设计模式
    装饰者模式(Decorator)是一种灵活的对象结构设计模式,旨在为单个对象动态地添加功能,而无需修改原有类的结构。通过封装对象并提供额外的行为,装饰者模式比传统的继承方式更加灵活和可扩展。例如,可以在运行时为特定对象添加边框或滚动条等特性,而不会影响其他对象。这种模式特别适用于需要在不同情况下动态组合功能的场景。 ... [详细]
  • 本文是Java并发编程系列的开篇之作,将详细解析Java 1.5及以上版本中提供的并发工具。文章假设读者已经具备同步和易失性关键字的基本知识,重点介绍信号量机制的内部工作原理及其在实际开发中的应用。 ... [详细]
  • VS2019 在创建 Windows 恢复点时出现卡顿问题及解决方法
    在使用 Visual Studio 2019 时,有时会在创建 Windows 恢复点时遇到卡顿问题。这可能是由于频繁的自动更新导致的,每次更新文件大小可能达到 1-2GB。尽管现代网络速度较快,但这些更新仍可能对系统性能产生影响。本文将探讨该问题的原因,并提供有效的解决方法,帮助用户提升开发效率。 ... [详细]
  • 深入解析:Synchronized 关键字在 Java 中对 int 和 Integer 对象的作用与影响
    深入探讨了 `Synchronized` 关键字在 Java 中对 `int` 和 `Integer` 对象的影响。尽管初看此题似乎简单,但其实质在于理解对象的概念。根据《Java编程思想》第二章的观点,一切皆为对象。本文详细分析了 `Synchronized` 关键字在不同数据类型上的作用机制,特别是对基本数据类型 `int` 和包装类 `Integer` 的区别处理,帮助读者深入理解 Java 中的同步机制及其在多线程环境中的应用。 ... [详细]
  • C++ 异步编程中获取线程执行结果的方法与技巧及其在前端开发中的应用探讨
    本文探讨了C++异步编程中获取线程执行结果的方法与技巧,并深入分析了这些技术在前端开发中的应用。通过对比不同的异步编程模型,本文详细介绍了如何高效地处理多线程任务,确保程序的稳定性和性能。同时,文章还结合实际案例,展示了这些方法在前端异步编程中的具体实现和优化策略。 ... [详细]
  • 在Linux系统中,网络配置是至关重要的任务之一。本文详细解析了Firewalld和Netfilter机制,并探讨了iptables的应用。通过使用`ip addr show`命令来查看网卡IP地址(需要安装`iproute`包),当网卡未分配IP地址或处于关闭状态时,可以通过`ip link set`命令进行配置和激活。此外,文章还介绍了如何利用Firewalld和iptables实现网络流量控制和安全策略管理,为系统管理员提供了实用的操作指南。 ... [详细]
  • C++ 开发实战:实用技巧与经验分享
    C++ 开发实战:实用技巧与经验分享 ... [详细]
author-avatar
mobiledu2502852915
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有