【死磕 Java 集合】— ConcurrentSkipListMap源码分析

转自:http://cmsblogs.com/?p=4773
 

做者:彤哥node

出处:https://www.cnblogs.com/tong-yuan/git

 

 

前情提要

 

点击连接查看“跳表”详细介绍。面试

 

拜托,面试别再问我跳表了!数组

 

简介

 

跳表是一个随机化的数据结构,实质就是一种能够进行二分查找的有序链表。微信

 

跳表在原有的有序链表上面增长了多级索引,经过索引来实现快速查找。数据结构

 

跳表不只能提升搜索性能,同时也能够提升插入和删除操做的性能。多线程

 

存储结构

 

跳表在原有的有序链表上面增长了多级索引,经过索引来实现快速查找。并发

 

skiplist3

 

源码分析

 

主要内部类

 

内部类跟存储结构结合着来看,大概能预测到代码的组织方式。app

 
// 数据节点,典型的单链表结构 static final class Node<K,V> { final K key; // 注意:这里value的类型是Object,而不是V // 在删除元素的时候value会指向当前元素自己 volatile Object value; volatile Node<K,V> next; Node(K key, Object value, Node<K,V> next) { this.key = key; this.value = value; this.next = next; } Node(Node<K,V> next) { this.key = null; this.value = this; // 当前元素自己(marker) this.next = next; } } // 索引节点,存储着对应的node值,及向下和向右的索引指针 static class Index<K,V> { final Node<K,V> node; final Index<K,V> down; volatile Index<K,V> right; Index(Node<K,V> node, Index<K,V> down, Index<K,V> right) { this.node = node; this.down = down; this.right = right; } } // 头索引节点,继承自Index,并扩展一个level字段,用于记录索引的层级 static final class HeadIndex<K,V> extendsIndex<K,V>{finalint level;HeadIndex(Node<K,V> node,Index<K,V> down,Index<K,V> right,int level){super(node, down, right);this.level = level;}}
 

(1)Node,数据节点,存储数据的节点,典型的单链表结构;

 

(2)Index,索引节点,存储着对应的node值,及向下和向右的索引指针;

 

(3)HeadIndex,头索引节点,继承自Index,并扩展一个level字段,用于记录索引的层级;

 

构造方法

 

public ConcurrentSkipListMap() { this.comparator = null; initialize(); } public ConcurrentSkipListMap(Comparator<? super K> comparator) { this.comparator = comparator; initialize(); } public ConcurrentSkipListMap(Map<? extends K, ? extends V> m) { this.comparator = null; initialize(); putAll(m); } public ConcurrentSkipListMap(SortedMap<K, ? extends V> m) { this.comparator = m.comparator(); initialize(); buildFromSorted(m); }
 

四个构造方法里面都调用了initialize()这个方法,那么,这个方法里面有什么呢?

 
private static final Object BASE_HEADER = new Object(); private void initialize() { keySet = null; entrySet = null; values = null; descendingMap = null; // Node(K key, Object value, Node<K,V> next) // HeadIndex(Node<K,V> node, Index<K,V> down, Index<K,V> right, int level) head = new HeadIndex<K,V>(new Node<K,V>(null, BASE_HEADER, null), null, null, 1); }
 

能够看到,这里初始化了一些属性,并建立了一个头索引节点,里面存储着一个数据节点,这个数据节点的值是空对象,且它的层级是1。

 

因此,初始化的时候,跳表中只有一个头索引节点,层级是1,数据节点是一个空对象,down和right都是null。

 

ConcurrentSkipList1

 

经过内部类的结构咱们知道,一个头索引指针包含node, down, right三个指针,为了便于理解,咱们把指向node的指针用虚线表示,其它两个用实线表示,也就是虚线不是代表方向的。

 

添加元素

 

经过【拜托,面试别再问我跳表了!】中的分析,咱们知道跳表插入元素的时候会经过抛硬币的方式决定出它须要的层级,而后找到各层链中它所在的位置,最后经过单链表插入的方式把节点及索引插入进去来实现的。

 

那么,ConcurrentSkipList中是这么作的吗?让咱们一块儿来探个究竟:

 
public V put(K key, V value) { // 不能存储value为null的元素 // 由于value为null标记该元素被删除(后面会看到) if (value == null) throw new NullPointerException(); // 调用doPut()方法添加元素 return doPut(key, value, false); } private V doPut(K key, V value, boolean onlyIfAbsent) { // 添加元素后存储在z中 Node<K,V> z; // added node // key也不能为null if (key == null) throw new NullPointerException(); Comparator<? super K> cmp = comparator; // Part I:找到目标节点的位置并插入 // 这里的目标节点是数据节点,也就是最底层的那条链 // 自旋 outer: for (;;) { // 寻找目标节点以前最近的一个索引对应的数据节点,存储在b中,b=before // 并把b的下一个数据节点存储在n中,n=next // 为了便于描述,我这里把b叫作当前节点,n叫作下一个节点 for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) { // 若是下一个节点不为空 // 就拿其key与目标节点的key比较,找到目标节点应该插入的位置 if (n != null) { // v=value,存储节点value值 // c=compare,存储两个节点比较的大小 Object v; int c; // n的下一个数据节点,也就是b的下一个节点的下一个节点(孙子节点) Node<K,V> f = n.next; // 若是n不为b的下一个节点 // 说明有其它线程修改了数据,则跳出内层循环 // 也就是回到了外层循环自旋的位置,从头来过 if (n != b.next) // inconsistent read break; // 若是n的value值为空,说明该节点已删除,协助删除节点 if ((v = n.value) == null) { // n is deleted // todo 这里为啥会协助删除?后面讲 n.helpDelete(b, f); break; } // 若是b的值为空或者v等于n,说明b已被删除 // 这时候n就是marker节点,那b就是被删除的那个 if (b.value ==null|| v == n)// b is deletedbreak;// 若是目标key与下一个节点的key大// 说明目标元素所在的位置还在下一个节点的后面if((c = cpr(cmp, key, n.key))>0){// 就把当前节点日后移一位// 一样的下一个节点也日后移一位// 再从新检查新n是否为空,它与目标key的关系 b = n; n = f;continue;}// 若是比较时发现下一个节点的key与目标key相同// 说明链表中自己就存在目标节点if(c ==0){// 则用新值替换旧值,并返回旧值(onlyIfAbsent=false)if(onlyIfAbsent || n.casValue(v, value)){@SuppressWarnings("unchecked") V vv =(V)v;return vv;}// 若是替换旧值时失败,说明其它线程先一步修改了值,从头来过break;// restart if lost race to replace value}// 若是c<0,就往下走,也就是找到了目标节点的位置// else c < 0; fall through}// 有两种状况会到这里// 一是到链表尾部了,也就是n为null了// 二是找到了目标节点的位置,也就是上面的c<0// 新建目标节点,并赋值给z// 这里把n做为新节点的next// 若是到链表尾部了,n为null,这毫无疑问// 若是c<0,则n的key比目标key大,相妆于在b和n之间插入目标节点z z =newNode<K,V>(key, value, n);// 原子更新b的下一个节点为目标节点zif(!b.casNext(n, z))// 若是更新失败,说明其它线程先一步修改了值,从头来过break;// restart if lost race to append to b// 若是更新成功,跳出自旋状态break outer;}}// 通过Part I,目标节点已经插入到有序链表中了// Part II:随机决定是否须要创建索引及其层次,若是须要则创建自上而下的索引// 取个随机数int rnd =ThreadLocalRandom.nextSecondarySeed();// 0x80000001展开为二进制为10000000000000000000000000000001// 只有两头是1// 这里(rnd & 0x80000001) == 0// 至关于排除了负数(负数最高位是1),排除了奇数(奇数最低位是1)// 只有最高位最低位都不为1的数跟0x80000001作&操做才会为0// 也就是正偶数if((rnd &0x80000001)==0){// test highest and lowest bits// 默认level为1,也就是只要到这里了就会至少创建一层索引int level =1, max;// 随机数从最低位的第二位开始,有几个连续的1则level就加几// 由于最低位确定是0,正偶数嘛// 好比,1100110,level就加2while(((rnd >>>=1)&1)!=0)++level;// 用于记录目标节点创建的最高的那层索引节点Index<K,V> idx =null;// 取头索引节点(这是最高层的头索引节点)HeadIndex<K,V> h = head;// 若是生成的层数小于等于当前最高层的层级// 也就是跳表的高度不会超过现有高度if(level <=(max = h.level)){// 从第一层开始创建一条竖直的索引链表// 这条链表使用down指针链接起来// 每一个索引节点里面都存储着目标节点这个数据节点// 最后idx存储的是这条索引链表的最高层节点for(int i =1; i <= level;++i) idx =newIndex<K,V>(z, idx,null);}else{// try to grow by one level// 若是新的层数超过了现有跳表的高度// 则最多只增长一层// 好比如今只有一层索引,那下一次最多增长到两层索引,增长多了也没有意义 level = max +1;// hold in array and later pick the one to use// idxs用于存储目标节点创建的竖起索引的全部索引节点// 其实这里直接使用idx这个最高节点也是能够完成的// 只是用一个数组存储全部节点要方便一些// 注意,这里数组0号位是没有使用的@SuppressWarnings("unchecked")Index<K,V>[] idxs =(Index<K,V>[])newIndex<?,?>[level+1];// 从第一层开始创建一条竖的索引链表(跟上面同样,只是这里顺便把索引节点放到数组里面了)for(int i =1; i <= level;++i) idxs[i]= idx =newIndex<K,V>(z, idx,null);// 自旋for(;;){// 旧的最高层头索引节点 h = head;// 旧的最高层级int oldLevel = h.level;// 再次检查,若是旧的最高层级已经不比新层级矮了// 说明有其它线程先一步修改了值,从头来过if(level <= oldLevel)// lost race to add levelbreak;// 新的最高层头索引节点HeadIndex<K,V> newh = h;// 头节点指向的数据节点Node<K,V> oldbase = h.node;// 超出的部分创建新的头索引节点for(int j = oldLevel+1; j <= level;++j) newh =newHeadIndex<K,V>(oldbase, newh, idxs[j], j);// 原子更新头索引节点if(casHead(h, newh)){// h指向新的最高层头索引节点 h = newh;// 把level赋值为旧的最高层级的// idx指向的不是最高的索引节点了// 而是与旧最高层平齐的索引节点 idx = idxs[level = oldLevel];break;}}}// 通过上面的步骤,有两种状况// 一是没有超出高度,新建一条目标节点的索引节点链// 二是超出了高度,新建一条目标节点的索引节点链,同时最高层头索引节点一样往上长// Part III:将新建的索引节点(包含头索引节点)与其它索引节点经过右指针链接在一块儿// 这时level是等于旧的最高层级的,自旋 splice:for(int insertionLevel = level;;){// h为最高头索引节点int j = h.level;// 从头索引节点开始遍历// 为了方便,这里叫q为当前节点,r为右节点,d为下节点,t为目标节点相应层级的索引for(Index<K,V> q = h, r = q.right, t = idx;;){// 若是遍历到了最右边,或者最下边,// 也就是遍历到头了,则退出外层循环if(q ==null|| t ==null)break splice;// 若是右节点不为空if(r !=null){// n是右节点的数据节点,为了方便,这里直接叫右节点的值Node<K,V> n = r.node;// 比较目标key与右节点的值int c = cpr(cmp, key, n.key);// 若是右节点的值为空了,则表示此节点已删除if(n.value ==null){// 则把右节点删除if(!q.unlink(r))// 若是删除失败,说明有其它线程先一步修改了,从头来过break;// 删除成功后从新取右节点 r = q.right;continue;}// 若是比较c>0,表示目标节点还要往右if(c >0){// 则把当前节点和右节点分别右移 q = r; r = r.right;continue;}}// 到这里说明已经到当前层级的最右边了// 这里实际是会先走第二个if// 第一个if// j与insertionLevel相等了// 实际是先走的第二个if,j自减后应该与insertionLevel相等if(j == insertionLevel){// 这里是真正连右指针的地方if(!q.link(r, t))// 链接失败,从头来过break;// restart// t节点的值为空,多是其它线程删除了这个元素if(t.node.value ==null){// 这里会去协助删除元素 findNode(key);break splice;}// 当前层级右指针链接完毕,向下移一层继续链接// 若是移到了最下面一层,则说明都链接完成了,退出外层循环if(--insertionLevel ==0)break splice;}// 第二个if// j先自减1,再与两个level比较// j、insertionLevel和t(idx)三者是对应的,都是还未把右指针连好的那个层级if(--j >= insertionLevel && j < level)// t往下移 t = t.down;// 当前层级到最右边了// 那只能往下一层级去走了// 当前节点下移// 再取相应的右节点 q = q.down; r = q.right;}}}returnnull;}// 寻找目标节点以前最近的一个索引对应的数据节点privateNode<K,V> findPredecessor(Object key,Comparator<?super K> cmp){// key不能为空if(key ==null)thrownewNullPointerException();// don't postpone errors// 自旋for(;;){// 从最高层头索引节点开始查找,先向右,再向下// 直到找到目标位置以前的那个索引for(Index<K,V> q = head, r = q.right, d;;){// 若是右节点不为空if(r !=null){// 右节点对应的数据节点,为了方便,咱们叫右节点的值Node<K,V> n = r.node; K k = n.key;// 若是右节点的value为空// 说明其它线程把这个节点标记为删除了// 则协助删除if(n.value ==null){if(!q.unlink(r))// 若是删除失败// 说明其它线程先删除了,从头来过break;// restart// 删除以后从新读取右节点 r = q.right;// reread rcontinue;}// 若是目标key比右节点还大,继续向右寻找if(cpr(cmp, key, k)>0){// 往右移 q = r;// 从新取右节点 r = r.right;continue;}// 若是c<0,说明不能再往右了}// 到这里说明当前层级已经到最右了// 两种状况:一是r==null,二是c<0// 再从下一级开始找// 若是没有下一级了,就返回这个索引对应的数据节点if((d = q.down)==null)return q.node;// 往下移 q = d;// 从新取右节点 r = d.right;}}}// Node.class中的方法,协助删除元素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. */// 这里的调用者this==n,三者关系是b->n->fif(f ==next&&this== b.next){// 将n的值设置为null后,会先把n的下个节点设置为marker节点// 这个marker节点的值是它本身// 这里若是不是它本身说明marker失败了,从新markerif(f ==null|| f.value != f)// not already marked casNext(f,newNode<K,V>(f));else// marker过了,就把b的下个节点指向marker的下个节点 b.casNext(this, f.next);}}// Index.class中的方法,删除succ节点finalboolean unlink(Index<K,V> succ){// 原子更新当前节点指向下一个节点的下一个节点// 也就是删除下一个节点return node.value !=null&& casRight(succ, succ.right);}// Index.class中的方法,在当前节点与succ之间插入newSucc节点finalboolean link(Index<K,V> succ,Index<K,V> newSucc){// 在当前节点与下一个节点中间插入一个节点Node<K,V> n = node;// 新节点指向当前节点的下一个节点 newSucc.right = succ;// 原子更新当前节点的下一个节点指向新节点return n.value !=null&& casRight(succ, newSucc);}
 

咱们这里把整个插入过程分红三个部分:

 

Part I:找到目标节点的位置并插入

 

(1)这里的目标节点是数据节点,也就是最底层的那条链;

 

(2)寻找目标节点以前最近的一个索引对应的数据节点(数据节点都是在最底层的链表上);

 

(3)从这个数据节点开始日后遍历,直到找到目标节点应该插入的位置;

 

(4)若是这个位置有元素,就更新其值(onlyIfAbsent=false);

 

(5)若是这个位置没有元素,就把目标节点插入;

 

(6)至此,目标节点已经插入到最底层的数据节点链表中了;

 

Part II:随机决定是否须要创建索引及其层次,若是须要则创建自上而下的索引

 

(1)取个随机数rnd,计算(rnd & 0x80000001);

 

(2)若是不等于0,结束插入过程,也就是不须要建立索引,返回;

 

(3)若是等于0,才进入建立索引的过程(只要正偶数才会等于0);

 

(4)计算while (((rnd >>>= 1) & 1) != 0),决定层级数,level从1开始;

 

(5)若是算出来的层级不高于现有最高层级,则直接创建一条竖直的索引链表(只有down有值),并结束Part II;

 

(6)若是算出来的层级高于现有最高层级,则新的层级只能比现有最高层级多1;

 

(7)一样创建一条竖直的索引链表(只有down有值);

 

(8)将头索引也向上增长到相应的高度,结束Part II;

 

(9)也就是说,若是层级不超过现有高度,只创建一条索引链,不然还要额外增长头索引链的高度(脑补一下,后面举例说明);

 

Part III:将新建的索引节点(包含头索引节点)与其它索引节点经过右指针链接在一块儿(补上right指针)

 

(1)从最高层级的头索引节点开始,向右遍历,找到目标索引节点的位置;

 

(2)若是当前层有目标索引,则把目标索引插入到这个位置,并把目标索引前一个索引向下移一个层级;

 

(3)若是当前层没有目标索引,则把目标索引位置前一个索引向下移一个层级;

 

(4)一样地,再向右遍历,寻找新的层级中目标索引的位置,回到第(2)步;

 

(5)依次循环找到全部层级目标索引的位置并把它们插入到横向的索引链表中;

 

总结起来,一共就是三大步:

 

(1)插入目标节点到数据节点链表中;

 

(2)创建竖直的down链表;

 

(3)创建横向的right链表;

 

添加元素举例

 

假设初始链表是这样:

 

ConcurrentSkipList2

 

假如,咱们如今要插入一个元素9。

 

(1)寻找目标节点以前最近的一个索引对应的数据节点,在这里也就是找到了5这个数据节点;

 

(2)从5开始向后遍历,找到目标节点的位置,也就是在8和12之间;

 

(3)插入9这个元素,Part I 结束;

 

ConcurrentSkipList3

 

而后,计算其索引层级,假如是3,也就是level=3。

 

(1)创建竖直的down索引链表;

 

(2)超过了现有高度2,还要再增长head索引链的高度;

 

(3)至此,Part II 结束;

 

ConcurrentSkipList4

 

最后,把right指针补齐。

 

(1)从第3层的head往右找当前层级目标索引的位置;

 

(2)找到就把目标索引和它前面索引的right指针连上,这里前一个正好是head;

 

(3)而后前一个索引向下移,这里就是head下移;

 

(4)再往右找目标索引的位置;

 

(5)找到了就把right指针连上,这里前一个是3的索引;

 

(6)而后3的索引下移;

 

(7)再往右找目标索引的位置;

 

(8)找到了就把right指针连上,这里前一个是5的索引;

 

(9)而后5下移,到底了,Part III 结束,整个插入过程结束;

 

ConcurrentSkipList5

 

是否是很简单^^

 

删除元素

 

删除元素,就是把各层级中对应的元素删除便可,真的这么简单吗?来让咱们上代码:

 
public V remove(Object key) { return doRemove(key, null); } final V doRemove(Object key, Object value) { // key不为空 if (key == null) throw new NullPointerException(); Comparator<? super K> cmp = comparator; // 自旋 outer: for (;;) { // 寻找目标节点以前的最近的索引节点对应的数据节点 // 为了方便,这里叫b为当前节点,n为下一个节点,f为下下个节点 for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) { Object v; int c; // 整个链表都遍历完了也没找到目标节点,退出外层循环 if (n == null) break outer; // 下下个节点 Node<K,V> f = n.next; // 再次检查 // 若是n不是b的下一个节点了 // 说明有其它线程先一步修改了,从头来过 if (n != b.next) // inconsistent read break; // 若是下个节点的值奕为null了 // 说明有其它线程标记该元素为删除状态了 if ((v = n.value) == null) { // n is deleted // 协助删除 n.helpDelete(b, f); break; } // 若是b的值为空或者v等于n,说明b已被删除 // 这时候n就是marker节点,那b就是被删除的那个 if (b.value == null || v == n) // b is deleted break; // 若是c<0,说明没找到元素,退出外层循环 if ((c = cpr(cmp, key, n.key)) < 0) break outer; // 若是c>0,说明还没找到,继续向右找 if(c >0){// 当前节点日后移 b = n;// 下一个节点日后移 n = f;continue;}// c=0,说明n就是要找的元素// 若是value不为空且不等于找到元素的value,不须要删除,退出外层循环if(value !=null&&!value.equals(v))break outer;// 若是value为空,或者相等// 原子标记n的value值为空if(!n.casValue(v,null))// 若是删除失败,说明其它线程先一步修改了,从头来过break;// P.S.到了这里n的值确定是设置成null了// 关键!!!!// 让n的下一个节点指向一个market节点// 这个market节点的key为null,value为marker本身,next为n的下个节点f// 或者让b的下一个节点指向下下个节点// 注意:这里是或者||,由于两个CAS不能保证都成功,只能一个一个去尝试// 这里有两层意思:// 一是若是标记market成功,再尝试将b的下个节点指向下下个节点,若是第二步失败了,进入条件,若是成功了就不用进入条件了// 二是若是标记market失败了,直接进入条件if(!n.appendMarker(f)||!b.casNext(n, f))// 经过findNode()重试删除(里面有个helpDelete()方法) findNode(key);// retry via findNodeelse{// 上面两步操做都成功了,才会进入这里,不太好理解,上面两个条件都有非"!"操做// 说明节点已经删除了,经过findPredecessor()方法删除索引节点// findPredecessor()里面有unlink()操做 findPredecessor(key, cmp);// clean index// 若是最高层头索引节点没有右节点,则跳表的高度降级if(head.right ==null) tryReduceLevel();}// 返回删除的元素值@SuppressWarnings("unchecked") V vv =(V)v;return vv;}}returnnull;}
 

(1)寻找目标节点以前最近的一个索引对应的数据节点(数据节点都是在最底层的链表上);

 

(2)从这个数据节点开始日后遍历,直到找到目标节点的位置;

 

(3)若是这个位置没有元素,直接返回null,表示没有要删除的元素;

 

(4)若是这个位置有元素,先经过n.casValue(v, null)原子更新把其value设置为null;

 

(5)经过n.appendMarker(f)在当前元素后面添加一个marker元素标记当前元素是要删除的元素;

 

(6)经过b.casNext(n, f)尝试删除元素;

 

(7)若是上面两步中的任意一步失败了都经过findNode(key)中的n.helpDelete(b, f)再去不断尝试删除;

 

(8)若是上面两步都成功了,再经过findPredecessor(key, cmp)中的q.unlink(r)删除索引节点;

 

(9)若是head的right指针指向了null,则跳表高度降级;

 

删除元素举例

 

假如初始跳表以下图所示,咱们要删除9这个元素。

 

ConcurrentSkipList6

 

(1)找到9这个数据节点;

 

(2)把9这个节点的value值设置为null;

 

(3)在9后面添加一个marker节点,标记9已经删除了;

 

(4)让8指向12;

 

(5)把索引节点与它前一个索引的right断开联系;

 

(6)跳表高度降级;

 

ConcurrentSkipList7

 

至于,为何要有(2)(3)(4)这么多步骤呢,由于多线程下若是直接让8指向12,能够其它线程先一步在9和12间插入了一个元素10呢,这时候就不对了。

 

因此这里搞了三步来保证多线程下操做的正确性。

 

若是第(2)步失败了,则直接重试;

 

若是第(3)或(4)步失败了,由于第(2)步是成功的,则经过helpDelete()不断重试去删除;

 

其实helpDelete()里面也是不断地重试(3)和(4);

 

只有这三步都正确完成了,才能说明这个元素完全被删除了。

 

这一块结合上面图中的红绿蓝色好好理解一下,必定要想在并发环境中会怎么样。

 

查找元素

 

通过上面的插入和删除,查找元素就比较简单了,直接上代码:

 
public V get(Object key) { return doGet(key); } private V doGet(Object key) { // key不为空 if (key == null) throw new NullPointerException(); Comparator<? super K> cmp = comparator; // 自旋 outer: for (;;) { // 寻找目标节点以前最近的索引对应的数据节点 // 为了方便,这里叫b为当前节点,n为下个节点,f为下下个节点 for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) { Object v; int c; // 若是链表到头还没找到元素,则跳出外层循环 if (n == null) break outer; // 下下个节点 Node<K,V> f = n.next; // 若是不一致读,从头来过 if (n != b.next) // inconsistent read break; // 若是n的值为空,说明节点已被其它线程标记为删除 if ((v = n.value) == null) { // n is deleted // 协助删除,再重试 n.helpDelete(b, f); break; } // 若是b的值为空或者v等于n,说明b已被删除 // 这时候n就是marker节点,那b就是被删除的那个 if (b.value == null || v == n) // b is deleted break; // 若是c==0,说明找到了元素,就返回元素值 if ((c = cpr(cmp, key, n.key)) == 0) { @SuppressWarnings("unchecked") V vv = (V)v; return vv; }// 若是c<0,说明没找到元素if(c <0)break outer;// 若是c>0,说明还没找到,继续寻找// 当前节点日后移 b = n;// 下一个节点日后移 n = f;}}returnnull;}
 

(1)寻找目标节点以前最近的一个索引对应的数据节点(数据节点都是在最底层的链表上);

 

(2)从这个数据节点开始日后遍历,直到找到目标节点的位置;

 

(3)若是这个位置没有元素,直接返回null,表示没有找到元素;

 

(4)若是这个位置有元素,返回元素的value值;

 

查找元素举例

 

假若有以下图所示这个跳表,咱们要查找9这个元素,它走过的路径是怎样的呢?可能跟你相像的不同。。

 

ConcurrentSkipList6

 

(1)寻找目标节点以前最近的一个索引对应的数据节点,这里就是5;

 

(2)从5开始日后遍历,通过8,到9;

 

(3)找到了返回;

 

整个路径以下图所示:

 

ConcurrentSkipList8

 

是否是很操蛋?

 

为啥不从9的索引直接过来呢?

 

从我实际打断点调试来看确实是按照上图的路径来走的。

 

我猜想多是由于findPredecessor()这个方法是插入、删除、查找元素多个方法共用的,在单链表中插入和删除元素是须要记录前一个元素的,而查找并不须要,这里为了兼容三者使得编码相对简单一点,因此就使用了一样的逻辑,而没有单独对查找元素进行优化。

 

不过也多是Doug Lea大神不当心写了个bug,若是有人知道缘由请告诉我。(公众号后台留言,新公众号的文章下面不支持留言了,蛋疼)

 

彩蛋

 

为何Redis选择使用跳表而不是红黑树来实现有序集合?

 

请查看【拜托,面试别再问我跳表了!】这篇文章。

 
赞(1)   打赏
 

 
 
 
如未加特殊说明,此网站文章均为原创,转载必须注明出处。 Java 技术驿站 »  【死磕 Java 集合】— ConcurrentSkipListMap源码分析
相关文章
相关标签/搜索