一篇文章搞懂 ConcurrentSkipListMap

2023-12-08 08:58

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

前言

本文隶属于专栏《100个问题搞定Java并发》,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢!

本专栏目录结构和参考文献请见100个问题搞定Java并发

正文

跳表

在 JDK 的并发包中,除常用的哈希表外,还实现了一种有趣的数据结构一一跳表。

跳表是一种可以用来快速查找的数据结构,有点类似于平衡树。

它们都可以对元素进行快速査找。

但一个重要的区别是:对平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整,而对跳表的插入和删除只需要对整个数据结构的局部进行操作即可。

这样带来的好处是:

在高并发的情况下,你会需要一个全局锁来保证整个平衡树的线程安全。

而对于跳表,你只需要部分锁即可。

这样,在高并发环境下,你就可以拥有更好的性能。

就查询的性能而言,因为跳表的时间复杂度是 O ( logn ),所以在并发数据结构中, JDK 使用跳表来实现一个 Map ,即 ConcurrentSkipListMap。

跳表的另外一个特点是随机算法。

跳表的本质是同时维护了多个链表,并且链表是分层的。下图是跳表结构示意图。

在这里插入图片描述

底层的链表维护了跳表内所有的元素,每上面一层链表都是下面一层链表的子集,一个元素插入哪些层是完全随机的。

因此,如果运气不好,你可能会得到一个性能很糟糕的结构。

但是在实际工作中,它的表现是非常好的。

跳表内的所有链表的元素都是排序的。

查找时,可以从顶级链表开始找。

一旦发现被査找的元素大于当前链表中的取值,就会转入下一层链表继续找。

这也就是说在查找过程中,搜索是跳跃式的。

查找 10

在跳表中査找元素 10 ,如上图所示。

查找从顶层的头部索引节点开始。

由于顶层的元素最少,因此可以快速跳过那些小于 10 的元素。

很快,查找过程就能到元素 10。

由于在第 2 层,元素 13 大于 10 ,故肯定无法在第 2 层找到元素 10 ,直接进入底层(包含所有元素)开始査找,并且很快就可以根据元素 9 搜索到元素 10 。 整个过程,要比一般链表从元素 1 开始逐个搜索快得多。

因此,很显然,跳表是一种使用空间换时间的算法。

跳表实现 Map 与 哈希算法实现 Map

使用跳表实现 Map 和使用哈希算法实现 Map 的一个不同之处是:

哈希并不会保存元素的顺序,而跳表内所有的元素都是有序的。

因此在对跳表进行遍历时,你会得到一个有序的结果。

因此,如果你的应用需要有序性,那么跳表就是你的最佳选择。

实践

下面展示了 ConcurrentSkipListMap 的简单使用方法。

package com.shockang.study.java.concurrent.skip_list;import java.util.Map;
import java.util.concurrent.ConcurrentSkipListMap;public class ConcurrentSkipListMapDemo {public static void main(String[] args) {Map<Integer, Integer> map = new ConcurrentSkipListMap<>();for (int i = 0; i < 30; i++) {map.put(i, i);}for (Map.Entry<Integer, Integer> entry : map.entrySet()) {System.out.println(entry.getKey());}}
}

源码分析(JDK8)

跳表的内部实现由几个关键的数据结构组成。

Node

首先是 Node ,一个 Node 表示一个节点,里面含有 key 和 value (就是 Map 的 key 和 value )两个重要的元素。

每个 Node 还会指向下个 Node ,因此还有一个元素 next 。

方法 casValue 用来设置 value的值,相对的 casNext() 方法用来设置next的字段。

    /*** Nodes hold keys and values, and are singly linked in sorted* order, possibly with some intervening marker nodes. The list is* headed by a dummy node accessible as head.node. The value field* is declared only as Object because it takes special non-V* values for marker and header nodes.*/static final class Node<K,V> {final K key;volatile Object value;volatile Node<K,V> next;/*** Creates a new regular node.*/Node(K key, Object value, Node<K,V> next) {this.key = key;this.value = value;this.next = next;}/*** Creates a new marker node. A marker is distinguished by* having its value field point to itself.  Marker nodes also* have null keys, a fact that is exploited in a few places,* but this doesn't distinguish markers from the base-level* header node (head.node), which also has a null key.*/Node(Node<K,V> next) {this.key = null;this.value = this;this.next = next;}/*** compareAndSet value field*/boolean casValue(Object cmp, Object val) {return UNSAFE.compareAndSwapObject(this, valueOffset, cmp, val);}/*** compareAndSet next field*/boolean casNext(Node<K,V> cmp, Node<K,V> val) {return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);}/*** Returns true if this node is a marker. This method isn't* actually called in any current code checking for markers* because callers will have already read value field and need* to use that read (not another done here) and so directly* test if value points to node.** @return true if this node is a marker node*/boolean isMarker() {return value == this;}/*** Returns true if this node is the header of base-level list.* @return true if this node is header node*/boolean isBaseHeader() {return value == BASE_HEADER;}/*** Tries to append a deletion marker to this node.* @param f the assumed current successor of this node* @return true if successful*/boolean appendMarker(Node<K,V> f) {return casNext(f, new Node<K,V>(f));}/*** Helps out a deletion by appending marker or unlinking from* predecessor. This is called during traversals when value* field seen to be null.* @param b predecessor* @param f successor*/void helpDelete(Node<K,V> b, Node<K,V> f) {/** Rechecking links and then doing only one of the* help-out stages per call tends to minimize CAS* interference among helping threads.*/if (f == next && this == b.next) {if (f == null || f.value != f) // not already markedcasNext(f, new Node<K,V>(f));elseb.casNext(this, f.next);}}/*** Returns value if this node contains a valid key-value pair,* else null.* @return this node's value if it isn't a marker or header or* is deleted, else null*/V getValidValue() {Object v = value;if (v == this || v == BASE_HEADER)return null;@SuppressWarnings("unchecked") V vv = (V)v;return vv;}/*** Creates and returns a new SimpleImmutableEntry holding current* mapping if this node holds a valid value, else null.* @return new entry or null*/AbstractMap.SimpleImmutableEntry<K,V> createSnapshot() {Object v = value;if (v == null || v == this || v == BASE_HEADER)return null;@SuppressWarnings("unchecked") V vv = (V)v;return new AbstractMap.SimpleImmutableEntry<K,V>(key, vv);}// UNSAFE mechanicsprivate static final sun.misc.Unsafe UNSAFE;private static final long valueOffset;private static final long nextOffset;static {try {UNSAFE = sun.misc.Unsafe.getUnsafe();Class<?> k = Node.class;valueOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("value"));nextOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("next"));} catch (Exception e) {throw new Error(e);}}}

Index

另外一个重要的数据结构是 Index 。

顾名思义,这个表示索引内部包装了 Node ,同时增加了向下的引用和向右的引用。

索引节点表示跳表的级别。

注意,尽管 Node 和 Index 都有前向字段,但它们的类型不同,处理方式也不同,将字段放置在共享抽象类中无法很好地捕获这些字段。

/*** Index nodes represent the levels of the skip list.  Note that* even though both Nodes and Indexes have forward-pointing* fields, they have different types and are handled in different* ways, that can't nicely be captured by placing field in a* shared abstract class.*/static class Index<K,V> {final Node<K,V> node;final Index<K,V> down;volatile Index<K,V> right;/*** Creates index node with given values.*/Index(Node<K,V> node, Index<K,V> down, Index<K,V> right) {this.node = node;this.down = down;this.right = right;}/*** compareAndSet right field*/final boolean casRight(Index<K,V> cmp, Index<K,V> val) {return UNSAFE.compareAndSwapObject(this, rightOffset, cmp, val);}/*** Returns true if the node this indexes has been deleted.* @return true if indexed node is known to be deleted*/final boolean indexesDeletedNode() {return node.value == null;}/*** Tries to CAS newSucc as successor.  To minimize races with* unlink that may lose this index node, if the node being* indexed is known to be deleted, it doesn't try to link in.* @param succ the expected current successor* @param newSucc the new successor* @return true if successful*/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);}/*** Tries to CAS right field to skip over apparent successor* succ.  Fails (forcing a retraversal by caller) if this node* is known to be deleted.* @param succ the expected current successor* @return true if successful*/final boolean unlink(Index<K,V> succ) {return node.value != null && casRight(succ, succ.right);}// Unsafe mechanicsprivate static final sun.misc.Unsafe UNSAFE;private static final long rightOffset;static {try {UNSAFE = sun.misc.Unsafe.getUnsafe();Class<?> k = Index.class;rightOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("right"));} catch (Exception e) {throw new Error(e);}}}

HeadIndex

整个跳表就是根据 Index 进行全网的组织的。

此外,对于每一层的表头还需要记录当前处于哪一层。

为此,还需要一个名为 Headlndex 的数据结构,表示链表头部的第一个 Index ,它继承自 Index 。

/*** Nodes heading each level keep track of their level.*/static final class HeadIndex<K,V> extends Index<K,V> {final int level;HeadIndex(Node<K,V> node, Index<K,V> down, Index<K,V> right, int level) {super(node, down, right);this.level = level;}}

这样核心的内部元素就介绍完了。

对于跳表的所有操作,就是组织好这些 Index之间的连接关系。

这篇关于一篇文章搞懂 ConcurrentSkipListMap的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

【数据结构】——原来排序算法搞懂这些就行,轻松拿捏

前言:快速排序的实现最重要的是找基准值,下面让我们来了解如何实现找基准值 基准值的注释:在快排的过程中,每一次我们要取一个元素作为枢纽值,以这个数字来将序列划分为两部分。 在此我们采用三数取中法,也就是取左端、中间、右端三个数,然后进行排序,将中间数作为枢纽值。 快速排序实现主框架: //快速排序 void QuickSort(int* arr, int left, int rig

java计算机毕设课设—停车管理信息系统(附源码、文章、相关截图、部署视频)

这是什么系统? 资源获取方式在最下方 java计算机毕设课设—停车管理信息系统(附源码、文章、相关截图、部署视频) 停车管理信息系统是为了提升停车场的运营效率和管理水平而设计的综合性平台。系统涵盖用户信息管理、车位管理、收费管理、违规车辆处理等多个功能模块,旨在实现对停车场资源的高效配置和实时监控。此外,系统还提供了资讯管理和统计查询功能,帮助管理者及时发布信息并进行数据分析,为停车场的科学

CSP-J基础之数学基础 初等数论 一篇搞懂(一)

文章目录 前言声明初等数论是什么初等数论历史1. **古代时期**2. **中世纪时期**3. **文艺复兴与近代**4. **现代时期** 整数的整除性约数什么样的整数除什么样的整数才能得到整数?条件:举例说明:一般化: 判断两个数能否被整除 因数与倍数质数与复合数使用开根号法判定质数哥德巴赫猜想最大公因数与辗转相除法计算最大公因数的常用方法:举几个例子:例子 1: 计算 12 和 18

文章解读与仿真程序复现思路——电力自动化设备EI\CSCD\北大核心《考虑燃料电池和电解槽虚拟惯量支撑的电力系统优化调度方法》

本专栏栏目提供文章与程序复现思路,具体已有的论文与论文源程序可翻阅本博主免费的专栏栏目《论文与完整程序》 论文与完整源程序_电网论文源程序的博客-CSDN博客https://blog.csdn.net/liang674027206/category_12531414.html 电网论文源程序-CSDN博客电网论文源程序擅长文章解读,论文与完整源程序,等方面的知识,电网论文源程序关注python

CSP-J基础之数学基础 初等数论 一篇搞懂(二)

文章目录 前言算术基本定理简介什么是质数?举个简单例子:重要的结论:算术基本定理公式解释:举例: 算术基本定理的求法如何找出质因数:举个简单的例子: 重要的步骤:C++实现 同余举个例子:同余的性质简介1. 同余的自反性2. 同余的对称性3. 同余的传递性4. 同余的加法性质5. 同余的乘法性质 推论 总结 前言 在计算机科学和数学中,初等数论是一个重要的基础领域,涉及到整数

【Linux】萌新看过来!一篇文章带你走进Linux世界

🚀个人主页:奋斗的小羊 🚀所属专栏:Linux 很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~ 目录 前言💥1、初识Linux💥1.1 什么是操作系统?💥1.2 各种操作系统对比💥1.3 现代Linux应用💥1.4 Linux常用版本 💥2、Linux 和 Windows 目录结构对比💥2.1 文件系统组织方式💥2.2

多线程的系列文章

Java多线程学习(一)Java多线程入门 Java多线程学习(二)synchronized关键字(1)   Java多线程学习(二)synchronized关键字(2) Java多线程学习(三)volatile关键字 Java多线程学习(四)等待/通知(wait/notify)机制 Java多线程学习(五)线程间通信知识点补充 Java多线程学习(六)Lock锁的使用 Java多

缓存的常见问题 以及解决博客文章

1.jedispool 连 redis 高并发卡死  (子非鱼yy) https://blog.csdn.net/ztx114/article/details/78291734 2. Redis安装及主从配置 https://blog.csdn.net/ztx114/article/details/78320193 3.Spring中使用RedisTemplate操作Redis(sprin

java计算机毕设课设—企业员工信息管理系统(附源码、文章、相关截图、部署视频)

这是什么系统? 获取资料方式在最下方 java计算机毕设课设—企业员工信息管理系统(附源码、文章、相关截图、部署视频) 企业员工信息管理系统旨在为公司提供高效的员工信息管理解决方案。该系统的核心功能涵盖密码修改、员工管理、部门管理、出勤管理、工资管理、请假审核等方面,帮助企业优化人力资源管理流程。系统结构如下: (1)前端(员工端): 1.密码修改:员工可以修改自己的密码,提升账户的安全

AI产品经理:ai产品经理从零基础到精通,非常详细收藏我这一篇就够了

在互联网的浪潮中,AI人工智能领域无疑是最引人注目的风口。AI产品经理,作为这一领域的新兴岗位,以其高薪、低压力、无年龄限制等优势,吸引了众多互联网从业者的目光。随着GPT等AIGC工具的兴起,AI产品经理的市场需求日益增长。 AI产品经理需不需要懂算法?🤔‍‍‍ AI产品经理不必像算法工程师那样精通算法,但必须能够与算法工程师有效沟通,了解如何管理AI项目,协调项目资源。 成功转行AI产