JUC源码解析-ConcurrentSkipListMap

2023-12-26 00:58

本文主要是介绍JUC源码解析-ConcurrentSkipListMap,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

单线程下若想使用有序的键值对,我们选用 TreeMap,若是考虑到线程安全问题,则可以使用 ConcurrentSkipListMap,它与ConcurrentSkipListSet之间的关系就如 TreeMap 与 TreeSet 之家的关系一样,set 由 map来实现,本篇主要来分析 ConcurrentSkipListMap。

底层数据结构是 跳表 Skip List:

  • 跳表分为多层,层级越高跳跃性越大,数据越少
  • 跳表的层级是通过“掷硬币”方式来决定增长的,也就是说在增加元素时,概率增长层数
  • 查找数据时,按照从上到下,从左往右的顺序查找
  • 时间复杂度O(log n),空间复杂度O(n)

跳表的实现:
在这里插入图片描述
三种类型的节点:
Node:键值对存储在其中,next字段构成了单向链表

Index:跳表中的索引节点,包含了右指针(right),向下的索引(down)和持有一个Node对象的引用。right 指向右侧的Index,down 指向下层的Index。

HeadIndex:继承自Index,每一条从左至右的Index 链的头节点,多了一个标识当前层级的level。

跳表结构模型:下层是Node 的单向链表,其上引用其的 Index 节点链,利用 Index 节点的 right 与 down 构成一个二维 Index 链,并非每个Node上都有Index 链,新的 Node 节点是否要构建 Index 链以及构建几层都是堆积的,沿着 Index 链从左至右,从上至下的搜寻过程就是所谓的

初始化时结构如下:
在这里插入图片描述
BASE_HEADER是个空对象,是 Node 链的头节点,其上是 HeadIndex 链,每一个HeadIndex 都是该层 Index 链的头节点;

private static final Object BASE_HEADER = new Object();

ConcurrentSkipListMap#head 指向 HeadIndex 链的顶层

private transient volatile HeadIndex<K,V> head;

HeadIndex 有个 level 字段,用于标识当前层级。

整个跳表结构如下图:
在这里插入图片描述
首先从 head 开始,沿着Index节点链从左至右,从上至下进行搜索,找到合适位置插入,新Node节点是否要在其上创建Index链,要创建几层Index,是随机的,下面分析源码时再说。

跳表结构分为两种链,一个是 Index 二维链表,另一个是 Node 数据链。

在Index 二维链中,每次只需遍历几个Index节点就可找到要 get / put / remove 位置附近的前置 Node 节点,然后从该前置节点处往后遍历Node链找寻,通常也只需较少的遍历就可找到。

并发下 Index 节点的插入与删除:

Index 的 down , node 字段是 final 的,不会改变,不存在安全问题。

Index 节点的 next 指针是 volatile 的,使用 CAS 添加/删除 Index 节点,这保证了安全性,对于失败的线程会采用重试的方式,让其再从头开始遍历搜寻,这是种退让的处理方式,过度的竞争反而影响性能。CAS + volatile 确实保证了单个节点操作的安全性,但是对于整个链在并发下的正确性还需要其它的保护措施:空值节点禁止执行 link / unlink 操作,对于失败的重试机制,以及主动的避让。我会在接下来对它们进行分析。

节点的 Index 链无论插入还是删除都不是一下全部完成,这就造成 Index 二维链表不能即时更新,但这并不会影响安全性,因为我们仍然可以靠着原来的 Index 链来查找,会对性能造成影响。

来看看 Index 节点的连接与断开操作 :link , unlink

        final boolean link(Index<K,V> succ, Index<K,V> newSucc) {Node<K,V> n = node;newSucc.right = succ;return n.value != null && casRight(succ, newSucc);}

首先将 newSucc 指向 succ,之后 CAS 将当前Index指向 newSucc,这样一次CAS 就能完成插入操作。竞争导致失败的话,调用方会采取重试的方式来处理。

如果当前 Index 持有的 Node 节点被删除 (指的是其value为null,这代表被删除状态,虽然仍可以访问到)是无法调用 link 方法的,因为这有丢失 Index 节点的风险。在文章后面的论述里,对此做了详细解释。

unlink

        final boolean unlink(Index<K,V> succ) {return node.value != null && casRight(succ, succ.right);}

不允许删除节点的 Index 执行unlink操作,为了防止情况的复杂性。
能调用unlink说明该Index关联的Node之前并未被删除且其next节点是删除状态的节点,因此调用unlink解绑,不过在调用前该节点也被删除了,处理方法就是使其unlink失败,强制让其重试,重试意味着从Index 链头开始重新搜寻。这是为了防止情况的复杂性。

可以看出 Index 的链接/断开都与 Node 节点删除操作的设计息息相关。

关于 Node 链的删除操作:

有些并发下列表的删除操作是在删除时标记已删除节点的 next 指针,以避免与并发插入冲突,以及在遍历时跟踪三元组(前置、节点、后续)以检测何时以及如何取消这些已删除节点的链接。出于性能和空间的考虑ConcurrentSkipListMap没采用这种方式。

ConcurrentSkipListMap 使用 volatile 的 next 指针,并使用 CAS 更改它们。删除时,它们会被更改指向一个新节点上,该节点可以看作是种 “ 标记 ” (该特殊的节点应该具有某种特殊的标识,比如该类中标记节点的key为null,value指向自身)

除了使用删除标记外,列表还使用节点 value 字段的空性(null)来指示删除,延迟删除方案。如果一个节点的 value 为 null,那么我们就认为它已被删除,即使它仍然可以被访问到。

为什么要在要删除的节点后CAS添加一个特殊的标记节点?value为null不就已经可以标识节点的状态是删除了吗?
重点在于CAS,通过主动的CAS来式该节点的其它CAS操作失败,或因竞争或因next改变,总之为删除节点尽量排除其它操作的干扰,那些失败的线程会重试。

以下是在 Node 节点链里删除 n 节点,其前置节点是b,后续节点是f,最初是:

 *        +------+       +------+      +------+*   ...  |   b  |------>|   n  |----->|   f  | ...*        +------+       +------+      +------+

1,CAS 将 n 的 value 从非空 置为 null
之后遍历到该节点的其它操作将视其为已被删除而做相应处理。其他正在进行的插入和删除操作可能仍然会修改 n 的 next 指针,因为其仍在链内。

2,CAS 将 n 的 next 指向标记节点 marker ( 该节点的key为null,value指向自身) 。之后,对于想将节点插入到 n 后的操作,在检测到 n.next 也就是 marker 后,不会进行插入操作。

 *        +------+       +------+      +------+       +------+*   ...  |   b  |------>|   n  |----->|marker|------>|   f  | ...*        +------+       +------+      +------+       +------+

3,CAS 将 b 的 next 指向 f 。此后不会在链表中遍历到 n,它最终可以被 gc 掉。

 *        +------+                                    +------+*   ...  |   b  |----------------------------------->|   f  | ...*        +------+                                    +------+

对于上面的这三步:

对于步骤 1 来说,若是由于与另一个操作的竞争而失败,重试即可。

步骤2-3可能会失败,一些操作在遍历时发现空值节点会帮助进行删除操作,所以存在竞争,采用这种帮助删除的方式而非等待,是为了防止操作因删除节点的影响而迟迟得不到执行,所以主动帮忙执行删除任务。

标记节点的使用稍微使帮助代码变得复杂,因为遍历必须跟踪四个节点(b、n、marker、f)的一致读取,而不仅仅是(b、n、f),不过标记节点 marker 的 next 字段是不会被改变的,空值节点指向 marker 的 next 也是不会再被更改的。这是因为竞争失败的会重试,重试发现空值节点会 helpDelete,帮完忙后会再次重试,所以说 n.next 和 marker.next 是不变的。

(b, n, f) 是三个Node节点,分别表示 (前置节点,当前节点,后置节点),称他们为跟踪三元组。

源码解析

并发下的情况是复杂的,状态不断的变化,通常执行一操作,会先获取此刻状态的一个快照,然后进行分析处理,只要在最后将结果进行正确的同步即可确保程序的线程安全,若是提交结果失败重试即可,重试获取最新的状态再次进行分析处理。

最重要的就是如何正确同步代码,接下来我们来看看ConcurrentSkipListMap是如何做的。

无论get,set ,remove 等都需要先沿着Index 二维链表从左到右从上到下找到相应位置附近的节点,这就是 findPredecessor 主要做的事,方法返回一个Node,之后就从该节点处开始遍历 Node 链,有了在Index二维链上的 跳 ,就避免了从头开始一个个遍历Node 链,也就节省了搜索时间。

findPredecessor

    private Node<K,V> findPredecessor(Object key, Comparator<? super K> cmp) {if (key == null)throw new NullPointerException(); // don't postpone errors// 两层for循环for (;;) {for (Index<K,V> q = head, r = q.right, d;;) {if (r != null) {Node<K,V> n = r.node;K k = n.key;// 发现被删除的Node,则unlink修正链// 若失败,break跳出当前循环,而外围是个无限的for循环,// 会重新读取head 开始搜索。if (n.value == null) {if (!q.unlink(r))break;           // restartr = q.right;         // reread rcontinue;}// 继续往右遍历Index链if (cpr(cmp, key, k) > 0) {q = r;r = r.right;continue;}}// 什么情况代表在Index二维链中找到了那个最靠近(从左至右

这篇关于JUC源码解析-ConcurrentSkipListMap的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/537578

相关文章

网页解析 lxml 库--实战

lxml库使用流程 lxml 是 Python 的第三方解析库,完全使用 Python 语言编写,它对 XPath表达式提供了良好的支 持,因此能够了高效地解析 HTML/XML 文档。本节讲解如何通过 lxml 库解析 HTML 文档。 pip install lxml lxm| 库提供了一个 etree 模块,该模块专门用来解析 HTML/XML 文档,下面来介绍一下 lxml 库

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

Java ArrayList扩容机制 (源码解读)

结论:初始长度为10,若所需长度小于1.5倍原长度,则按照1.5倍扩容。若不够用则按照所需长度扩容。 一. 明确类内部重要变量含义         1:数组默认长度         2:这是一个共享的空数组实例,用于明确创建长度为0时的ArrayList ,比如通过 new ArrayList<>(0),ArrayList 内部的数组 elementData 会指向这个 EMPTY_EL

如何在Visual Studio中调试.NET源码

今天偶然在看别人代码时,发现在他的代码里使用了Any判断List<T>是否为空。 我一般的做法是先判断是否为null,再判断Count。 看了一下Count的源码如下: 1 [__DynamicallyInvokable]2 public int Count3 {4 [__DynamicallyInvokable]5 get

工厂ERP管理系统实现源码(JAVA)

工厂进销存管理系统是一个集采购管理、仓库管理、生产管理和销售管理于一体的综合解决方案。该系统旨在帮助企业优化流程、提高效率、降低成本,并实时掌握各环节的运营状况。 在采购管理方面,系统能够处理采购订单、供应商管理和采购入库等流程,确保采购过程的透明和高效。仓库管理方面,实现库存的精准管理,包括入库、出库、盘点等操作,确保库存数据的准确性和实时性。 生产管理模块则涵盖了生产计划制定、物料需求计划、

OWASP十大安全漏洞解析

OWASP(开放式Web应用程序安全项目)发布的“十大安全漏洞”列表是Web应用程序安全领域的权威指南,它总结了Web应用程序中最常见、最危险的安全隐患。以下是对OWASP十大安全漏洞的详细解析: 1. 注入漏洞(Injection) 描述:攻击者通过在应用程序的输入数据中插入恶意代码,从而控制应用程序的行为。常见的注入类型包括SQL注入、OS命令注入、LDAP注入等。 影响:可能导致数据泄

从状态管理到性能优化:全面解析 Android Compose

文章目录 引言一、Android Compose基本概念1.1 什么是Android Compose?1.2 Compose的优势1.3 如何在项目中使用Compose 二、Compose中的状态管理2.1 状态管理的重要性2.2 Compose中的状态和数据流2.3 使用State和MutableState处理状态2.4 通过ViewModel进行状态管理 三、Compose中的列表和滚动

Spring 源码解读:自定义实现Bean定义的注册与解析

引言 在Spring框架中,Bean的注册与解析是整个依赖注入流程的核心步骤。通过Bean定义,Spring容器知道如何创建、配置和管理每个Bean实例。本篇文章将通过实现一个简化版的Bean定义注册与解析机制,帮助你理解Spring框架背后的设计逻辑。我们还将对比Spring中的BeanDefinition和BeanDefinitionRegistry,以全面掌握Bean注册和解析的核心原理。

CSP 2023 提高级第一轮 CSP-S 2023初试题 完善程序第二题解析 未完

一、题目阅读 (最大值之和)给定整数序列 a0,⋯,an−1,求该序列所有非空连续子序列的最大值之和。上述参数满足 1≤n≤105 和 1≤ai≤108。 一个序列的非空连续子序列可以用两个下标 ll 和 rr(其中0≤l≤r<n0≤l≤r<n)表示,对应的序列为 al,al+1,⋯,ar​。两个非空连续子序列不同,当且仅当下标不同。 例如,当原序列为 [1,2,1,2] 时,要计算子序列 [