JDK1.8的ConcurrentSkipListMap的实现

今天在看redis的基本数据,结构时候,看到有一个跳跃表的数据结构,这之前我一直没听说过这种结构,于是先把redis的跳跃表结构的实现看了,然后他的名称是skiplist,这让我想起在java8的并发包下有带skiplist字样的2个实现类,我才明白,原来java也有这种数据结构,于是先百度了一些关于跳跃表的算法的原理,个人觉得下面这篇文章写的浅显易懂,对于入门可谓非常合适。先贴链接:http://blog.csdn.net/a1259109679/article/details/46442895。然后顺便看看java里面的这种数据结构是怎么实现的。

jdk8下有2个这种实现类,一个是本文介绍的ConcurrentSkipListMap,还有一个是ConcurrentSkipListSet,二者的区别在写本文之时还未阅读后者的源码。

这里需要先明白几个组成元素:node节点是真正用来存放我们存入的数据的,其中的key和value不必多说,同时还持有一个指向下一个node节点的指针。

index,这个对象有3个属性,第一个是一个node节点,第二个是down是指向下一个层级的index,right就是指向本层级的下一个index的指针。

头节点,第一个属性是代表所在的层级,跳跃表的快速定位依赖与此,然后还有一个指向下一个头结点的

下图是一个拥有12个node的分成2层的示意图,从图中蓝色的线表示一次寻找k7的过程,当然本次数据少,所以优势并未那么明显,但是如果数据量大的时候,能大大减少寻找次数。当然这里如果把一排node放在最先会更直观。

从上图可以看出,其实整个结构分为2块,一块是由我们的node的节点组成的基础数据,而另外一块是由一个多层level2组成的数据结构,且寻找过程总是从level最高的开始,然后先找到满足条件的最后一个index,然后通过index指向下一层对应的index,然后继续向右向下寻找,直到找到level1且满足了右边的node的key比我们需要查找的key大或者为null,然后进入到我们最先的node节点,然后去遍历链表。如下图所示

对整个结构以及寻找过程清楚以后,我们来看看源码中怎么样形成这个结构的。

首先来了解第一个API,put方法。put方法首先验证value为不为Null,也就是这不允许value为Null,这与map是不同的。真正做加入操作是交给我们的doPut方法操作的。如果度过spring的源码的同学知道,spring中很多的对外的API也都是做一个必要性验证,然后真正的操作都交给do**方法操作,而这一的方法一般要么是private的要么是protect的。

下面我们看doput方法。第三个参数是一个布尔值,从名字可以看到应该是一个仅仅在缺席时候加入,这里是不是可以猜测如果已经存在当前key,有2种操作,一种是修改,一种是加入。进来看这里首先验证了key不能为null,结合上一步的验证可以知道在我们这个跳跃表里是不允许键值为null.然后通过一个findPredecessor,传入当前新加入的key,和比较的方式来进行寻址。真正的寻找应该放在那里是在这里实现的。

findpredecessor从名字可以理解为寻找前辈,寻找前任。这个寻找过程是从最高level的headIndex开始的,然后先向右开始找,直到右为Null或者,右边Index的Node的key大于了当前需要加入的key为止,然后再向下寻找,同样也是直到下为null为止才是出口。通过一系列向右向下寻找,最后会落到我们的需要加入节点的前辈或者前任Node上,然后返回。

寻找前任的过程

然后我们来看看找到这个前辈后我们做什么。首先取得这个前辈的next,如果它的下一个Next节点不为空的时候,我们继续依着这个next去继续向下寻找,直到下一个节点为空为止。这里看最后一个if判断,如果c为0(代表找到了一个key一模一样的node),然后判断onlyIfAbsent,一般情况下默认传入的是false,如果是false,则执行第二个CAS替换,如果为true则不会执行。这验证了我们前面的猜测。这里有多2个判断,主要是在并发下的一些判断吧。能够完全做到线程安全。

下面看看如果下个节点以后的做法,直接把我们新传入的key和value包装成node,然后通过cas来完成next指向,然后就加到了我们的链中去了。到目前为止,其实整个加入过程最终操作的是我们的第一块,也就是整个node链条。

下面我们看加入完成后还做了些什么?如果让你想,你认为接下来会干什么呢?到目前为止,我们整个数据结构是怎么建立的?对,应该到了拆分层次的过程了。在原来的跳跃表原理中拆分解释了随机的,也就是掷骰子的方式,这在常理看来是不是有点不靠谱啊,在java中呢?通过源码可以知道,java中也是随机的。不过这个随机是由线程生成的唯一一个数字与一个16禁止数字做与操作后的结果来决定的。看下图:如果通过判断为true后,首先将level设置默认为1,然后对rnd进行一个右位移操作后在与1进行与操作后来确定需要分为几层。具体为什么用这个算法,我看的时候也不大明白,等将来搞明白后在补上。然后通过多次对level++后,就确定了我们需要分成几层了。确定最终需要的层次后首先判断,需要分成的level与当前的最大level比较,如果比后者小的话,然后对小于level的每层初始化一个index,每层的node都指向新加入的node,down指向下一层的同样的自己,右侧全部初始化为null。其实也就是如果不需要扩大层的时候,为每层初始化一个这个index,然后准备加入到原来的index链条中去

如果需要扩容的话,首先确定的是每次是扩容一层,然后初始化一个对应的indxs的数组,然后为每层都创建包含新加入z的node,并且指向下一层的自己的index。然后通过for循环来进行扩层操作。扩容是从目前最高的那一层开始的,先加一层,包装一个node指向当前head的node指向的node,第二个参数是down指向当前的head,index指向刚刚创建的对应层次的index,以及最后一个确定层次的编号。这样就完成了一层中的headindex的创建。然后通过cas吧当前的head与新加入层的head进行替换。这里用语言描述的肯定有些头晕,我们下面用个图来讲述一下

下图是一个直观图
完成了一个新加入层

但是我们发现完成了新加入,但是对于新加入的Node,只有最顶层建立了指针指向,其他层并未进行加入操作。接下来我们就要完成为每层加入index的操作。由名字可以看到就是插入层次的概念。

此时的h已经指向了我们最新的最高的层的headIndex,然后首先r指向第一个右侧index,如果这个index不为null,则先取出这个index的node,然后与当前新加入的key比较,拿到返回值额c,然后判断node的value不能为null,如果为null则先解除二者之间的关系,解除成功后从新建立right的指向,最后如果新加入的key比当前index的node的key大的话然后顺延继续找下一个index继续比较,下图1是横向的找,找到后进入第二个图。

从最顶层向右开始顺延index找

然后判断如果当前是最顶层,则把最后的r指向新加入的Index。然后对标志的插入层进行减减操作,然后同时对j(也是当前插入层),进行减减操作,且j必须必代表最高层的level小,然后把t指向了下一层(也就是为新加入的key包装的index,之前为每一层都建立一个且建立了down指向关系)。然后同样把headIndex和right同步下移。用文字描述同样比较苍白,用一张图来说明

黑色线代表当前操作,红色线代表完成建立关系后的操作,都开始指向下一层。

通过上面,就完成了新加入对象的寻址,分层,新关系建立的流程。

下面看remove方法,也很简单,是通过doremove完成的。

从renmove方法可以看出,即可以通过key移除,也可以通过同时满足key和value完成。

进来首先也是需要寻找到前key,这里一定是返回的是index所指向的node,也就是上一步一定是仅仅找到index,然后这里通过下面的代码继续向下寻找,找到后首先用CAS把value替换为null,然后判断是不是这层唯一的一个index,如果是的话就把这层给干掉。然后就完成了删除,从这里看出,它仅仅是把node的value设置为null或者删除掉本层。

代码接下面

size()方法,全局并没有维护一个总数,所以每次都会去遍历,这里的findFirst也就是直接找到第一个node,然后利用node的next去统计。最后最多能返回Integer.MAX_VALUE.这里在线程并发下是安全的

get方法,也是通过doGet方法完成的。这里不具体列代码了,也是很简单,通过findpredecessor方法定位到最近的index,然后继续顺延node去寻找定位返回。

replace方法是可以指定key和value来指定替换,也就是必须满足value是oldvalue。首先通过findNode找到对应的node。

读完这里也就完成了整个代码解读。但是这里肯定会有疑问,也就是移除操作,其实真正的移除操作都是在其他操作里借用判断value是不是为null来进行操作的。这里最重要的是helpdelete方法。其实很简单,就是一个链条解除的关系。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容