转载请注明源出处:http://www.cnblogs.com/lighten/p/7542578.html

1.前言

  一个可伸缩的并发实现,这个map实现了排序功能,默认使用的是对象自身的compareTo方法,如果提供了比较器,使用比较器的比较方法。简单来说ConcurrentSkipListMap是TreeMap的并发实现,但是为什么没有称之为ConcurrentTreeMap呢?这和其自身的实现有关。该类是SkipLists的变种实现,提供了log(n)的时间开销:containsKey、get、put、remove。Insertion, removal, update, and access等操作都是线程安全的。迭代器是弱一致性的,升序迭代器比降序的快。该map的size方法不是常量时间开销,需要遍历,所以这个值在并发的时候可能不准。该map也不允许空键或值。

2.ConcurrentSkipListMap

2.1 实现原理

  此类实现了一个二维树状跳跃链表,index level由持有数据的基本节点的独立节点表示。有两个原因采取这种方法,而不是使用数组:1.数组实现复杂性更高,开销更大,2.我们可以使用开销更小的算法来完成大量遍历的索引列表而不是用于基本链表。下图是一个基础的两级索引的list结构:

  基础的算法使用的是the HM linked ordered set algorithm的变种。这些算法的基本原理是在删除节点时标记删除节点的下一个节点指针,以避免并发插入冲突,当遍历跟踪三元组(前置节点,当前结点,后继节点)时,决定何时以及如何将这些已删除的结点断开。

  节点直接使用CAS标记next指针,而不是使用标记位来标记列表删除。在删除时,不是标记一个指针,而是在另一个被认为是标记指针的结点中进行拼接。此外使用删除标记,链表中也有空的元素被看成是删除,这类似于懒删除模式。如果一个节点的值为null,就被认为是逻辑删除了,并忽略。下面是删除一个节点的示意图,删除n节点,初始状态如下:

  1.CAS设置n的值从非null变成null。从这个时刻开始,没有public操作会认为这个节点存在,然而其它正在进行的insert和delete可能依旧在改变其next指针。

  2.CAS设置n的next指针为一个新的标记节点,当这个节点存在时,没有其它的结点能添加在n后面,这是为了防止删除错误。

  3.CAS设置b的next指针,越过n和marker节点,这个时刻开始,没有遍历方法能够访问到n,其可以被垃圾回收了。

  第一步失败会导致简单的重试,2,3步失败了也不要紧,因为其他操作会忽视null节点,并且会帮助逻辑删除节点从链表移除。

2.2 数据结构

  上图是一个数据结构,链表的头结点,比较器,和一个头结点的对象。

  Node节点也比较标准,键值和下一个节点,内部方法如下:

    casValue:CAS设置结点的value

    casNext:CAS设置结点的next

    isMarker:判断该节点是否是Marker节点,依据就是Marker节点的value就是其本身

    isBaseHeader:判断该节点是否是头结点,依据就是head节点的value是类的BASE_HEADER的对象

    appendMarker:CAS设置结点的next节点为marker节点,参数是该节点原来的next结点

    helpDelete:帮助删除节点(当该节点是value为null的时候)

    getValidValue:返回当前结点的值

    createSnapshot:创建该节点的键值对快照,是一个不可改变的集合

  Index就是该类的数据结构了,其对Node继续了封装,多了down和right节点,这是一个跳跃表的基本结构。里面的方法如下:

    casRight:CAS设置该节点的right指针

    indexesDeletedNode:判断该节点是否被逻辑删除了

    link:CAS设置新的后继节点,参数是原后继节点和新的后继节点

    unlink:CAS设置该节点的后继节点的后继节点为该节点的后继节点,就是将该节点的后继节点移除

  HeadIndex继承自Index,补充了Index缺少了level字段。

2.3 基本操作

  获取一个键值对:

  步骤如下:

    1.通过key找到跳跃表key的前一个节点b,该key的键值就是在这个节点b的后面。

    2.如果b的next节点n为null,意味着b为最后节点,没有元素可找,跳出循环

    3.再次检测b的next节点是否是n,不是意味着被put抢先插入了,重新找前置节点进行循环

    4.n节点值为null,已逻辑删除,helpDelete帮助移除该节点,跳出循环,重新找前置节点

    5.b节点value为null,或n==n.value(marked节点)b被移除,跳出循环,重新找前置节点

    6.比较n的key和获取的key,相等就返回值,<0就跳出循环。没找到就继续判断n.next.

  查找指定key开始遍历的跳跃表前置节点方法如上图,步骤如下:

    1.从头结点开始遍历,当前结点的右节点为r

    2.r不为null,但是值为null,尝试移除,移除失败重新从head开始遍历,成功继续找下一个跳跃点,继续遍历。如果r值不为null,但是比较出来key的值要大,意味着还可以跳跃这段,继续找下一个跳跃点。

    3.找到合适的跳跃点,就去找该跳跃点的起始节点,down存在就是要当前跳跃点的结点,存在就在down中查找合适的跳跃点。

  如果对这个结构有疑惑的,可以参考:这里。来理解一下什么是跳跃表。

  放入一个元素:

    private V doPut(K key, V value, boolean onlyIfAbsent) {
Node<K,V> z; // added node
if (key == null)
throw new NullPointerException();
Comparator<? super K> cmp = comparator;
outer: for (;;) {
for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
if (n != null) {
Object v; int c;
Node<K,V> f = n.next;
if (n != b.next) // inconsistent read
break;
if ((v = n.value) == null) { // n is deleted
n.helpDelete(b, f);
break;
}
if (b.value == null || v == n) // b is deleted
break;
if ((c = cpr(cmp, key, n.key)) > 0) {
b = n;
n = f;
continue;
}
if (c == 0) {
if (onlyIfAbsent || n.casValue(v, value)) {
@SuppressWarnings("unchecked") V vv = (V)v;
return vv;
}
break; // restart if lost race to replace value
}
// else c < 0; fall through
} z = new Node<K,V>(key, value, n);
if (!b.casNext(n, z))
break; // restart if lost race to append to b
break outer;
}
} int rnd = ThreadLocalRandom.nextSecondarySeed();
if ((rnd & 0x80000001) == 0) { // test highest and lowest bits
int level = 1, max;
while (((rnd >>>= 1) & 1) != 0)
++level;
Index<K,V> idx = null;
HeadIndex<K,V> h = head;
if (level <= (max = h.level)) {
for (int i = 1; i <= level; ++i)
idx = new Index<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
@SuppressWarnings("unchecked")Index<K,V>[] idxs =
(Index<K,V>[])new Index<?,?>[level+1];
for (int i = 1; i <= level; ++i)
idxs[i] = idx = new Index<K,V>(z, idx, null);
for (;;) {
h = head;
int oldLevel = h.level;
if (level <= oldLevel) // lost race to add level
break;
HeadIndex<K,V> newh = h;
Node<K,V> oldbase = h.node;
for (int j = oldLevel+1; j <= level; ++j)
newh = new HeadIndex<K,V>(oldbase, newh, idxs[j], j);
if (casHead(h, newh)) {
h = newh;
idx = idxs[level = oldLevel];
break;
}
}
}
// find insertion points and splice in
splice: for (int insertionLevel = level;;) {
int j = h.level;
for (Index<K,V> q = h, r = q.right, t = idx;;) {
if (q == null || t == null)
break splice;
if (r != null) {
Node<K,V> n = r.node;
// compare before deletion check avoids needing recheck
int c = cpr(cmp, key, n.key);
if (n.value == null) {
if (!q.unlink(r))
break;
r = q.right;
continue;
}
if (c > 0) {
q = r;
r = r.right;
continue;
}
} if (j == insertionLevel) {
if (!q.link(r, t))
break; // restart
if (t.node.value == null) {
findNode(key);
break splice;
}
if (--insertionLevel == 0)
break splice;
} if (--j >= insertionLevel && j < level)
t = t.down;
q = q.down;
r = q.right;
}
}
}
return null;
}

  put方法主要经历了2个步骤:

  1:查找合适的位置,插入该节点。

      1)查找前置跳跃点b,其next节点为n,

   2)遍历查找合适的插入点,n为null就创建节点,添加在b的next节点,添加成功跳出第一步,失败重新进行1)

   3)n不为null,就查找其应该插入的节点,先要判断n是否还是b的next节点,防止被抢先在中间插入了,再判断n节点是否是有效节点,逻辑删除了就回到1)再重来。最后判断b节点是否被删除了。后面如果key的大小大于n节点的k,意味着还要往后找,如果等于就替换掉该节点的值,跳出第一步。最后找到了合适的插入点就尝试插入,失败重来,成功结束第一步。整个过程的逻辑和get的类似。

  2:构建跳跃表的结点,调整跳表。完成第一步仅仅是将节点插入了链表中,还需要完成跳表的构成。(级别就意味着跳表的间隔,级别越大同一级别的结点越少,间隔越大,这种方式在查找的时候可以提升查找速度,从最大的级别开始,逐级定位结点)

   1)随机级别,偶数且大于0。随机方法不说明。

   2)如果该级别比头结点要小,生成一系列头结点的down节点(Index结点包含的node,自然是步骤1插入的结点),从级别1开始

   3)该级别比头结点级别高,加大一个级别,生成从1开始的所有级别结点(node为插入节点)构成down链。

   4)再次判断头结点级别,如果head级别比该级别高,证明被抢先调整了,重来。没有抢先,重新构建头结点索引headIndex,node是头结点的node,补充缺失的级别就可以了。替换头结点HeadIndex成功跳出循环,失败重来。

    上面都是构建down方向的结点,确保head的down方向包含了所有索引级别。后面的方法就是构建right方法的连接了。这里要注意,h变成了新的头结点,level却是旧的级别。

   5)h结点或h的right结点r为null,没必要进行,结束该环节

   6)r不为null,比较key和r的结点n的key,n结点被逻辑删除,就帮助其移除,移除后找下一个r结点。当前r结点要小于key,则key还在右边,继续找r。直到找到key应该在的位置,即r结点>=key,key的right就是r。

   7)不断降级,直到找到当前的插入级别,直到到指定级别,构建连接,连接失败重来,成功如果构建的结点被逻辑删除了,通过findNode方法,删除它。

  整个过程有些抽象,结合二维图看会比较清楚,首先是一维的有序链表,这个就是Node结点,但是跳表为了加快搜索速度,使用了检索级别indexlevel构成了二维图。之前也提过,indexlevel级别越高,间隔越大,结点越少。一个新加结点,首先要确定其属于几级,1级就不需要构建IndexNode,一系列判断出其所属级别后,就先构建down方向的一系列结点,再通过头结点,将整个right方向结点联通,这个就是一个基本的思路。由于从头结点开始遍历,所以头结点必须有最高的级别。所以新节点基本超过头结点的时候,要提升头结点级别。大体逻辑就是这样。

  其它的方法不再进行介绍,上面基本能了解ConcurrentSkipListMap的基本原理。

Java之集合(二十六)ConcurrentSkipListMap的更多相关文章

  1. Java开发学习(二十六)----SpringMVC返回响应结果

    SpringMVC接收到请求和数据后,进行了一些处理,当然这个处理可以是转发给Service,Service层再调用Dao层完成的,不管怎样,处理完以后,都需要将结果告知给用户. 比如:根据用户ID查 ...

  2. Java进阶专题(二十六) 将近2万字的Dubbo原理解析,彻底搞懂dubbo

    前言 ​ 前面我们研究了RPC的原理,市面上有很多基于RPC思想实现的框架,比如有Dubbo.今天就从Dubbo的SPI机制.服务注册与发现源码及网络通信过程去深入剖析下Dubbo. Dubbo架构 ...

  3. Java从零开始学二十六(包装类)

    一.包装类 包装类是将基本类型封装到一个类中.也就是将基本数据类型包装成一个类类型. java程序设计为每一种基本类型都提供了一个包装类.这些包装类就在java.lang包中.有8个包装类 二.包装类 ...

  4. Java基础(二十六)Java IO(3)字节流(Byte Stream)

    字节流是以字节为单位来处理数据的,由于字节流不会对数据进行任何转换,因此用来处理二进制的数据. 一.InputStream类与OutputStream类 1.InputStream类是所有字节输入流的 ...

  5. Java之集合(二十四)ConcurrentLinkedDeque

    转载请注明源出处:http://www.cnblogs.com/lighten/p/7517454.html 1.前言 本章介绍并发队列ConcurrentLinkedDeque,这是一个非阻塞,无锁 ...

  6. Java之集合(二十二)PriorityBlockingQueue

    转载请注明源出处:http://www.cnblogs.com/lighten/p/7510799.html 1.前言 本章介绍阻塞队列PriorityBlockingQueue.这是一个无界有序的阻 ...

  7. Java学习笔记二十六:Java多态中的引用类型转换

    Java多态中的引用类型转换 引用类型转换: 1.向上类型转换(隐式/自动类型转换),是小类型到大类型的转换: 2.向下类型转换(强制类型转换),是大类型到小类型的转换: 3.instanceof运算 ...

  8. java 面向对象(二十六):枚举类的使用

    1. 枚举类的说明:* 1.枚举类的理解:类的对象只有有限个,确定的.我们称此类为枚举类* 2.当需要定义一组常量时,强烈建议使用枚举类* 3.如果枚举类中只一个对象,则可以作为单例模式的实现方式. ...

  9. Java进阶专题(二十六) 数据库原理研究与优化

    前言 在一个大数据量的系统中,这些数据的存储.处理.搜索是一个非常棘手的问题. 比如存储问题:单台服务器的存储能力及数据处理能力都是有限的, 因此需要增加服务器, 搭建集群来存储海量数据. 读写性能问 ...

随机推荐

  1. 22. Valuing Water 珍惜水资源

    . Valuing Water 珍惜水资源 ① Humanity uses a little less than half the water available worldwide.Yet occu ...

  2. Libevent学习之SocketPair实现

    Libevent设计的精化之一在于把Timer事件.Signal事件和IO事件统一集成在一个Reactor中,以统一的方式去处理这三种不同的事件,更确切的说是把Timer事件和Signal事件融合到了 ...

  3. Axios的基本使用

    Axios的基本使用 介绍 Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中.在vue 中,用来发ajax请求,与后端交互. 从浏览器中创建 XMLHtt ...

  4. HDU 1716 排列2 (格式问题+排列)

    题意:. 析:我们完全可以STL里面的函数next_permutation(),然后方便,又简单,这个题坑就是在格式上. 行末不能有空格,结尾不能有空行,不大好控制,必须控制好第一次数. 这个题本应该 ...

  5. oss上传文件夹-cloud2-泽优软件

    泽优软件云存储上传控件(cloud2)支持上传整个文件夹,并在云空间中保留文件夹的层级结构,同时在数据库中也写入层级结构信息.文件与文件夹层级结构关系通过id,pid字段关联. 本地文件夹结构 文件 ...

  6. 如何将word中的图片和文字导入自己的博客中

    目前大部分的博客作者在用Word写博客这件事情上都会遇到以下3个痛点: 1.所有博客平台关闭了文档发布接口,用户无法使用Word,Windows Live Writer等工具来发布博客.使用Word写 ...

  7. Ubuntu安装教程(双系统)

    经常要重装还不如写个安装教程省的每次都要查 Ubuntu安装教程: win7下安装Linux实现双系统全攻略:https://jingyan.baidu.com/article/c275f6bacc3 ...

  8. (KMP扩展 利用循环节来计算) Cyclic Nacklace -- hdu -- 3746

    http://acm.hdu.edu.cn/showproblem.php?pid=3746 Cyclic Nacklace Time Limit: 2000/1000 MS (Java/Others ...

  9. 转一篇做BI项目的好文

    首先,我们有一个大的假设前提,集团报表平台是服务于大型公司,比如有很多分公司,子公司,多部门等,并且有BI需求的访问人群超过1000以上的公司. 这样,我们的关键词是:集团 平台 运营 集团:意味着, ...

  10. [ASP.NET].NET逻辑分层架构总结

    一.基础知识准备: 1.层的原则: (1)每一层以接口方式供上层调用. (2)上层只能调用下层. (3)依赖分为松散交互和严格交互两种. 2.业务逻辑分类: (1)应用逻辑. (2)领域逻辑. 3.采 ...