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

相关文章

线上Java OOM问题定位与解决方案超详细解析

《线上JavaOOM问题定位与解决方案超详细解析》OOM是JVM抛出的错误,表示内存分配失败,:本文主要介绍线上JavaOOM问题定位与解决方案的相关资料,文中通过代码介绍的非常详细,需要的朋... 目录一、OOM问题核心认知1.1 OOM定义与技术定位1.2 OOM常见类型及技术特征二、OOM问题定位工具

深度解析Python中递归下降解析器的原理与实现

《深度解析Python中递归下降解析器的原理与实现》在编译器设计、配置文件处理和数据转换领域,递归下降解析器是最常用且最直观的解析技术,本文将详细介绍递归下降解析器的原理与实现,感兴趣的小伙伴可以跟随... 目录引言:解析器的核心价值一、递归下降解析器基础1.1 核心概念解析1.2 基本架构二、简单算术表达

深度解析Java @Serial 注解及常见错误案例

《深度解析Java@Serial注解及常见错误案例》Java14引入@Serial注解,用于编译时校验序列化成员,替代传统方式解决运行时错误,适用于Serializable类的方法/字段,需注意签... 目录Java @Serial 注解深度解析1. 注解本质2. 核心作用(1) 主要用途(2) 适用位置3

Java MCP 的鉴权深度解析

《JavaMCP的鉴权深度解析》文章介绍JavaMCP鉴权的实现方式,指出客户端可通过queryString、header或env传递鉴权信息,服务器端支持工具单独鉴权、过滤器集中鉴权及启动时鉴权... 目录一、MCP Client 侧(负责传递,比较简单)(1)常见的 mcpServers json 配置

从原理到实战解析Java Stream 的并行流性能优化

《从原理到实战解析JavaStream的并行流性能优化》本文给大家介绍JavaStream的并行流性能优化:从原理到实战的全攻略,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的... 目录一、并行流的核心原理与适用场景二、性能优化的核心策略1. 合理设置并行度:打破默认阈值2. 避免装箱

Maven中生命周期深度解析与实战指南

《Maven中生命周期深度解析与实战指南》这篇文章主要为大家详细介绍了Maven生命周期实战指南,包含核心概念、阶段详解、SpringBoot特化场景及企业级实践建议,希望对大家有一定的帮助... 目录一、Maven 生命周期哲学二、default生命周期核心阶段详解(高频使用)三、clean生命周期核心阶

深入解析C++ 中std::map内存管理

《深入解析C++中std::map内存管理》文章详解C++std::map内存管理,指出clear()仅删除元素可能不释放底层内存,建议用swap()与空map交换以彻底释放,针对指针类型需手动de... 目录1️、基本清空std::map2️、使用 swap 彻底释放内存3️、map 中存储指针类型的对象

Java Scanner类解析与实战教程

《JavaScanner类解析与实战教程》JavaScanner类(java.util包)是文本输入解析工具,支持基本类型和字符串读取,基于Readable接口与正则分隔符实现,适用于控制台、文件输... 目录一、核心设计与工作原理1.底层依赖2.解析机制A.核心逻辑基于分隔符(delimiter)和模式匹

Java+AI驱动实现PDF文件数据提取与解析

《Java+AI驱动实现PDF文件数据提取与解析》本文将和大家分享一套基于AI的体检报告智能评估方案,详细介绍从PDF上传、内容提取到AI分析、数据存储的全流程自动化实现方法,感兴趣的可以了解下... 目录一、核心流程:从上传到评估的完整链路二、第一步:解析 PDF,提取体检报告内容1. 引入依赖2. 封装

深度解析Python yfinance的核心功能和高级用法

《深度解析Pythonyfinance的核心功能和高级用法》yfinance是一个功能强大且易于使用的Python库,用于从YahooFinance获取金融数据,本教程将深入探讨yfinance的核... 目录yfinance 深度解析教程 (python)1. 简介与安装1.1 什么是 yfinance?