SkipList和java中ConcurrentSkipListMap的实现

简介

一开始听说SkipList我是一脸懵逼的,啥?还有SkipList?这个是什么玩意。

后面经过我的不断搜索和学习,终于明白了SkipList原来是一种数据结构,而java中的ConcurrentSkipListMap和ConcurrentSkipListSet就是这种结构的实现。

接下来就让我们一步一步的揭开SkipList和ConcurrentSkipListMap的面纱吧。

SkipList

先看下维基百科中SkipList的定义:

SkipList是一种层级结构。最底层的是排序过的最原始的linked list。

往上是一层一层的层级结构,每个底层节点按照一定的概率出现在上一层list中。这个概率叫做p,通常p取1/2或者1/4。

先设定一个函数f,可以随机产生0和1这两个数,并且这两个数出现的几率是一样的,那么这时候的p就是1/2。

对每个节点,我们这样操作:

我们运行一次f,当f=1时,我们将该节点插入到上层layer的list中去。当f=0时,不插入。

举个例子,上图中的list中有10个排序过的节点,第一个节点默认每层都有。对于第二个节点,运行f=0,不插入。对于第三个节点,运行f=1,将第三个节点插入layer 1,以此类推,最后得到的layer 1 list中的节点有:1,3,4,6,9。

然后我们再继续往上构建layer。 最终得到上图的SkipList。

通过使用SkipList,我们构建了多个List,包含不同的排序过的节点,从而提升List的查找效率。

我们通过下图能有一个更清晰的认识:

每次的查找都是从最顶层开始,因为最顶层的节点数最少,如果要查找的节点在list中的两个节点中间,则向下移一层继续查找,最终找到最底层要插入的位置,插入节点,然后再次调用概率函数f,决定是否向上复制节点。

其本质上相当于二分法查找,其查找的时间复杂度是O(logn)。

ConcurrentSkipListMap

ConcurrentSkipListMap是一个并发的SkipList,那么它具有两个特点,SkipList和concurrent。我们分别来讲解。

SkipList的实现

上面讲解了SkipList的数据结构,接下来看下ConcurrentSkipListMap是怎么实现这个skipList的:

ConcurrentSkipListMap中有三种结构,base nodes,Head nodes和index nodes。

base nodes组成了有序的链表结构,是ConcurrentSkipListMap的最底层实现。

    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;
}
}

上面可以看到每个Node都是一个k,v的entry,并且其有一个next指向下一个节点。

index nodes是构建SkipList上层结构的基本节点:

    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;
}
}

从上面的构造我们可以看到,Index节点包含了Node节点,除此之外,Index还有两个指针,一个指向同一个layer的下一个节点,一个指向下一层layer的节点。

这样的结构可以方便遍历的实现。

最后看一下HeadIndex,HeadIndex代表的是Head节点:

    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;
}
}

HeadIndex和Index很类似,只不过多了一个level字段,表示所在的层级。

在ConcurrentSkipListMap初始化的时候,会初始化HeadIndex:

head = new HeadIndex<K,V>(new Node<K,V>(null, BASE_HEADER, null),null, null, 1);

我们可以看到HeadIndex中的Node是key=null,value=BASE_HEADER的虚拟节点。初始的level=1。

concurrent的实现

接下来,我们再看一下并发是怎么实现的:

基本上并发类都是通过UNSAFE.compareAndSwapObject来实现的,ConcurrentSkipListMap也不例外。

假如我们有三个节点,b-n-f。现在需要删除节点n。

第一步,使用CAS将n的valu的值从non-null设置为null。这个时候,任何外部的操作都会认为这个节点是不存在的。但是那些内部的插入或者删除操作还是会继续修改n的next指针。

第二步,使用CAS将n的next指针指向一个新的marker节点,从这个时候开始,n的next指针将不会指向任何其他的节点。

我们看下marker节点的定义:

        Node(Node<K,V> next) {
this.key = null;
this.value = this;
this.next = next;
}

我们可以看到marker节点实际上是一个key为null,value是自己的节点。

第三步,使用CAS将b的next指针指向f。从这一步起,n节点不会再被其他的程序访问,这意味着n可以被垃圾回收了。

我们思考一下为什么要插入一个marker节点,这是因为我们在删除的时候,需要告诉所有的线程,节点n准备被删除了,因为n本来就指向f节点,这个时候需要一个中间节点来表示这个准备删除的状态。

总结

本文从SkipList数据结构开始,讲解了ConcurrentSkipListMap的实现。希望大家能够喜欢。

欢迎关注我的公众号:程序那些事,更多精彩等着您!

更多内容请访问:flydean的博客

SkipList和java中ConcurrentSkipListMap的实现的更多相关文章

  1. 万字长文深入理解java中的集合-附PDF下载

    目录 1. 前言 2. List 2.1 fail-safe fail-fast知多少 2.1.1 Fail-fast Iterator 2.1.2 Fail-fast 的原理 2.1.3 Fail- ...

  2. Java中的线程到底有哪些安全策略

    摘要:Java中的线程到底有哪些安全策略呢?本文就为你彻底分析下! 本文分享自华为云社区<[高并发]线程安全策略>,作者:冰 河 . 一.不可变对象 不可变对象需要满足的条件 (1)对象创 ...

  3. Java中的多线程技术全面详解

    本文主要从整体上介绍Java中的多线程技术,对于一些重要的基础概念会进行相对详细的介绍,若有叙述不清晰或是不正确的地方,希望大家指出,谢谢大家:) 为什么使用多线程 并发与并行 我们知道,在单核机器上 ...

  4. Java多线程并发08——锁在Java中的应用

    前两篇文章中,为各位带来了,锁的类型及锁在Java中的实现.接下来本文将为各位带来锁在Java中的应用相关知识.关注我的公众号「Java面典」了解更多 Java 相关知识点. 锁在Java中主要应用还 ...

  5. java中的锁

    java中有哪些锁 这个问题在我看了一遍<java并发编程>后尽然无法回答,说明自己对于锁的概念了解的不够.于是再次翻看了一下书里的内容,突然有点打开脑门的感觉.看来确实是要学习的最好方式 ...

  6. java中的字符串相关知识整理

    字符串为什么这么重要 写了多年java的开发应该对String不陌生,但是我却越发觉得它陌生.每学一门编程语言就会与字符串这个关键词打不少交道.看来它真的很重要. 字符串就是一系列的字符组合的串,如果 ...

  7. Java中的Socket的用法

                                   Java中的Socket的用法 Java中的Socket分为普通的Socket和NioSocket. 普通Socket的用法 Java中的 ...

  8. java中Action层、Service层和Dao层的功能区分

    Action/Service/DAO简介: Action是管理业务(Service)调度和管理跳转的. Service是管理具体的功能的. Action只负责管理,而Service负责实施. DAO只 ...

  9. Java中常用集合操作

    一.Map 名值对存储的. 常用派生类HashMap类 添加: put(key,value)往集合里添加数据 删除: clear()删除所有 remove(key)清除单个,根据k来找 获取: siz ...

  10. java中的移位运算符:<<,>>,>>>总结

    java中有三种移位运算符 <<      :     左移运算符,num << 1,相当于num乘以2 >>      :     右移运算符,num >& ...

随机推荐

  1. stat模块

    # stat模块定义了常数和函数,并用这些来解释os.stat().os.fstat()和os.lstat()的结果(如果这些在该平台上存在的话). stat.S_ISREG(mode) # 判断mo ...

  2. 【Azure Storage Account Table】询问批量将存储账户中的表嵌入另一个账户中的办法

    问题描述 询问批量将存储账户中的表嵌入另一个账户中的办法? 问题解答 方式一:使用 AzCopy 使用Az copy做表格的导入导出,注意您需要使用Azcopy 7.3版本来实现对Table的操作,可 ...

  3. 【Azure Redis 缓存】Azure Redis加入VNET后,在另一个区域(如中国东部二区)的VNET无法访问Redis服务(注:两个VNET已经结对,相互之间可以互ping)

    问题描述 为了保护Redis资源,把它与VNET集成后,实现只能通过VNET内网访问.在东二的区域中部署两个Redis服务后,发现一个奇怪的现象:东1区中的VM资源通过全局对等互联(Peering)实 ...

  4. win上vscode配置ffmpeg

    参考博客https://blog.csdn.net/leixiaohua1020/article/details/38868499 https://blog.csdn.net/weixin_37515 ...

  5. 使用 Docker 部署 File Browser 文件管理系统

    1)File Browser 介绍 官网:https://filebrowser.org/ GitHub:https://github.com/filebrowser/filebrowser 今天为大 ...

  6. 面试官:线程调用2次start会怎样?我支支吾吾没答上来

    写在开头 在写完上一篇文章<Java面试必考题之线程的生命周期,结合源码,透彻讲解!>后,本以为这个小知识点就总结完了. 但刚刚吃晚饭时,突然想到了多年前自己面试时的亲身经历,决定再回来补 ...

  7. 反编译和逆向出现:java.lang.VerifyError(新问题样本)

    PS:要转载请注明出处,本人版权所有. PS: 这个只是基于<我自己>的理解, 如果和你的原则及想法相冲突,请谅解,勿喷. 前置说明   本文作为本人csdn blog的主站的备份.(Bl ...

  8. springboot 配置 OpenFeign 时报错:Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; Connection refused: no further information

    报错内容如下: 2022-11-18 01:55:18.998 ERROR 22220 --- [nio-8086-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServl ...

  9. 您真的了解Java中的锁吗?这7种不同维度下的锁知道吗?

    写在开头 在上几篇博文中,我们聊到过volatile关键字,用它修饰变量可以保证可见性与有序性,但它并不是锁,在使用时并不会阻塞线程,且不保证原子性,属于一种轻量级.高效的同步方式,因此,如果我们的使 ...

  10. 新零售SaaS架构:线上商城系统架构设计

    零售商家为什么要建设线上商城? 传统的实体门店服务范围有限,只能吸引周边500米以内的消费者.因此,如何拓展服务范围,吸引更多的消费者到店,成为了店家迫切需要解决的问题. 缺乏忠实顾客,客户基础不稳, ...