Java同步数据结构之ConcurrentSkipListMap/ConcurrentSkipListSet

引言

上一篇Java同步数据结构之Map概述及ConcurrentSkipListMap原理已经将ConcurrentSkipListMap的原理大致搞清楚了,它是一种有序的能够实现高效插入,删除,更新,搜索的基于链表结构的无锁线程安全Map,它是SkipList(跳表)的一种变体实现(其节点同时存储了key和Value键值对),即是以空间换时间的方式随机的将基层链表上的某些节点抽出来构成多层索引链表,从而可以通过索引链表快速的定位到基层链表的目标节点。新的索引链表会随着其索引节点的第一次加入而建立,也会在当该索引链表上除了头索引节点HeadIndex之外的所有索引节点被删除而被移除,所以它是可伸缩的跳表。

ConcurrentSkipListMap不允许使用null作为键或值,因为无法可靠地区分null返回值与不存在的元素值。

源码解读

首先是看三个重要的内部类,它们分别表示节点类Node,索引类Index以及头索引HeadIndex,它们各自代表的含义很好理解,通过上一篇原理介绍中的图一目了然。

 1 static final class Node<K,V> {
 2     final K key;             //
 3     volatile Object value;     //
 4     volatile Node<K,V> next; //基层节点的next指针
 5     
 6     ...
 7 }    
 8     
 9 static class Index<K,V> {
10     final Node<K,V> node;  //索引节点
11     final Index<K,V> down;  //指向下层的指针
12     volatile Index<K,V> right; //指向同一层右侧的指针
13     
14     ....
15 }    
16 static final class HeadIndex<K,V> extends Index<K,V> {
17     final int level;  //当前的索引层级
18     HeadIndex(Node<K,V> node, Index<K,V> down, Index<K,V> right, int level) {
19         super(node, down, right);
20         this.level = level;
21     }
22 }
View Code

 这三个内都比较简单,其内部结构与上一篇原理介绍中的设定一致,Node是基层链表上保存了键值对的节点,Index是索引层上的节点,HeadIndex是索引层上的左边第一个索引节点即头索引节点。

接下来是一些基础字段和构造方法:

 1 public class ConcurrentSkipListMap<K,V> extends AbstractMap<K,V>
 2     implements ConcurrentNavigableMap<K,V>, Cloneable, Serializable {
 3     
 4 /**
 5  * 标记初始头索引节点的特殊值
 6  */
 7 private static final Object BASE_HEADER = new Object();
 8 
 9 //最高层索引的头索引节点
10 private transient volatile HeadIndex<K,V> head;
11 
12 final Comparator<? super K> comparator; //比较器
13 
14 //四个构造方法忽略
15 
16 //构造方法都会执行的初始化方法
17 private void initialize() {
18     keySet = null;
19     entrySet = null;
20     values = null;
21     descendingMap = null;
22     //初始化head
23     head = new HeadIndex<K,V>(new Node<K,V>(null, BASE_HEADER, null),
24                               null, null, 1);
25 }

 其中最重要的是它有一个指向最高层索引的头索引节点head,该节点就是所有操作的入口,不论什么操作都需要从该节点开始往右,往下一层层的找到与目标节点最接近的节点。四个构造方法就不说了,它们正是在上一篇Map概述中实现SortedMap的标配构造方法,它们不一例外都执行了initialize方法来初始化。初始化完成之后,一个空的ConcurrentSkipListMap实例的结果如下:

可见空Map时,存在一个虚拟节点作为唯一的基层节点,head正是指向该虚拟节点,并且虚拟节点的value是BASE_HEADER,它是一个Object对象,这也是为什么Node节点的Value属性没有声明成V,而是Object的原因,因为存在这也的特殊值用于内部实现的判断。

查找

在介绍其他所有操作之前,我们有必要先来看看其搜索查找的实现,因为这是其他所有操作几乎都会依赖的方法,毕竟不论你要干什么都先要找到目标节点或者最接近目标节点的节点。

 1 //通过索引找到比key小的索引节点对应的基层节点,如果没有(所有节点都比key大)则返回基层的head节点
 2 //其实就是从最高层索引往右下找到在第一个刚好比key小的基层节点,
 3 //接下来还需要调用findNode从该节点开始继续在基层上往右找到最接近key的节点
 4 private Node<K,V> findPredecessor(Object key, Comparator<? super K> cmp) {
 5     if (key == null)
 6         throw new NullPointerException(); // don't postpone errors
 7     for (;;) { //自旋
 8         //从最高层头索引节点开始查找,先向右,再向下
 9         for (Index<K,V> q = head, r = q.right, d;;) {
10             if (r != null) { //存在右节点
11                 Node<K,V> n = r.node;
12                 K k = n.key;
13                 if (n.value == null) { //是一个已标记为删除的节点
14                     if (!q.unlink(r))    //辅助删除失败,重新自旋
15                         break;           // restart
16                     r = q.right;         // 删除成功之后重新读取右节点
17                     continue;
18                 }
19                 if (cpr(cmp, key, k) > 0) {//目标key比当前r对应的节点大,继续向右寻找
20                     q = r;
21                     r = r.right;
22                     continue;
23                 }
24             }
25             //到这里说明要么已经走到当前索引层的最右边了,
26             //或者当前节点已经比目标节点大了
27             //已经到最基层了,返回刚好比目标key小的节点即q.node
28             if ((d = q.down) == null)
29                 return q.node;
30             //还没到最底层,跳到下一层,继续向右寻找
31             q = d;
32             r = d.right;
33         }
34     }
35 } 
36 
37 //在基层上找到与key相等的节点
38 private Node<K,V> findNode(Object key) {
39     if (key == null)
40         throw new NullPointerException(); // don't postpone errors
41     Comparator<? super K> cmp = comparator;
42     outer: for (;;) { //自旋
43         //从通过索引拿到的第一个比key小的基层节点b开始
44         for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
45             Object v; int c;
46             if (n == null) //搜索到头了,退出吧
47                 break outer;
48             Node<K,V> f = n.next;
49             if (n != b.next)                // 不一致的读,说明链表有变化,重新开始
50                 break;
51             if ((v = n.value) == null) {    // 下一个节点n已经被标记要删除
52                 n.helpDelete(b, f);            //辅助删除节点n,然后重新开始
53                 break;
54             }
55             if (b.value == null || v == n)  // 节点b已经被删除,重新开始
56                 break;
57             if ((c = cpr(cmp, key, n.key)) == 0) //找到与目标节点相等的节点了
58                 return n;                        //返回相等的节点
59             if (c < 0) //当前节点比目标节点大,说明比存在比目标key小的节点,退出吧
60                 break outer;
61             //说明当前节点比目标key小,继续向右靠近
62             b = n;
63             n = f;
64         }
65     }
66     return null;
67 } 
View Code

findPredecessor()方法就是从最高层索引的头节点head开始通过层层索引找到最接近目标节点的索引节点对应在基层链表上的节点,例如在上一篇跳表的查找部分,在拥有二级索引的跳表中要查找22,findPredecessor就会从最高层的3开始,经过3,17,17找到一级索引上的17这个索引节点,并返回它在基层链表上对应的节点17.

findNode()方法就是在findPredecessor返回的节点基础上继续在基层链表上往右靠近目标节点,并返回找到的目标节点,例如findPredecessor拿到基层节点17了,接下来是17, 19 , 22这样就找到了。

另外,若我们要找的节点刚好存在索引,例如我们上一篇跳表的查找部分,在拥有二级索引的跳表中要找25,明明已经存在25的索引节点,但是由于findPredecessor只能返回接近目标节点的节点,而不能是相等的节点,所以还是会经过3,17,17,17,19,22,25这样的路径来靠近25.

V get(key)---获取操作

其实其逻辑的方法doGet与findNode的逻辑基本一致,只不过返回找到节点的值,就不重复细说了。

V put(key, value) --- 插入/替换操作

1 public V put(K key, V value) {
2     if (value == null)
3         throw new NullPointerException();
4     return doPut(key, value, false);
5 }

 可见put操作由doPut实现,下面是其源码:

  1 //插入元素的实现,onlyIfAbsent为true,表示不存在对应的key才插入键值对,
  2 //否则如果存在对应的key会做替换操作。
  3 private V doPut(K key, V value, boolean onlyIfAbsent) {
  4     Node<K,V> z;             // added node
  5     if (key == null)
  6         throw new NullPointerException();
  7     Comparator<? super K> cmp = comparator;
  8     //第一部分:在基层链上找到合适的位置插入
  9     outer: for (;;) {//自旋
 10         //从通过索引拿到的第一个比key小的基层节点b开始
 11         for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
 12             if (n != null) { //基层链上b的右边还存在节点
 13                 Object v; int c;
 14                 Node<K,V> f = n.next;
 15                 if (n != b.next)               // 不一致的读,说明链表有变化,重新开始
 16                     break;
 17                 if ((v = n.value) == null) {   // 下一个节点n已经被标记要删除
 18                     n.helpDelete(b, f);        //辅助删除节点n,然后重新开始
 19                     break;
 20                 }
 21                 if (b.value == null || v == n) // 节点b已经被删除,重新开始
 22                     break;
 23                 if ((c = cpr(cmp, key, n.key)) > 0) {//key比当前节点n大,遍历朝右挪
 24                     b = n;
 25                     n = f;
 26                     continue;
 27                 }
 28                 if (c == 0) { //说明链表中已经存在了相同的key
 29                     //如果不需要替换或者替换成功,直接返回旧值
 30                     if (onlyIfAbsent || n.casValue(v, value)) {
 31                         @SuppressWarnings("unchecked") V vv = (V)v;
 32                         return vv;
 33                     }
 34                     //替换失败(其它线程抢先替换了),重新再来
 35                     break; // restart if lost race to replace value
 36                 }
 37                 // else c < 0; fall through
 38             }
 39             //到这里说明搜索到链表结尾了,或者找到了刚好比key大的节点n
 40             //如果到链表尾部了,n为null那么key就是新的尾节点,否则就需要把key插入n的前面
 41             z = new Node<K,V>(key, value, n);
 42             if (!b.casNext(n, z)) //插入失败重新再来
 43                 break;         // restart if lost race to append to b
 44             break outer;    //成功插入key,跳出第一部分
 45         }
 46     }
 47     //到这里说明成功将key插入了,替换旧值不会到这里
 48     //第二部分:随机决定是否需要建立索引节点及其层次,如果需要则建立自下而上的索引组
 49     int rnd = ThreadLocalRandom.nextSecondarySeed();
 50     if ((rnd & 0x80000001) == 0) { // test highest and lowest bits
 51         //通过随机数若要建立索引节点
 52         int level = 1, max;
 53         while (((rnd >>>= 1) & 1) != 0)
 54             ++level; //通过随机数计算出索引的层级
 55         Index<K,V> idx = null;
 56         HeadIndex<K,V> h = head;
 57         if (level <= (max = h.level)) { //索引层级小于当前最高层级
 58             for (int i = 1; i <= level; ++i)
 59                 idx = new Index<K,V>(z, idx, null); //创建每一层的索引节点,已经把down指针设置好
 60         }
 61         else { // 产生的层级大于当前最高层级,需要新增一层索引
 62             level = max + 1; // hold in array and later pick the one to use
 63             @SuppressWarnings("unchecked")Index<K,V>[] idxs =
 64                 (Index<K,V>[])new Index<?,?>[level+1]; //存储该新节点每一层的索引节点
 65             for (int i = 1; i <= level; ++i)
 66                 idxs[i] = idx = new Index<K,V>(z, idx, null);//创建每一层的索引节点,已经把down指针设置好
 67             for (;;) { //自旋
 68                 h = head;
 69                 int oldLevel = h.level;
 70                 if (level <= oldLevel) // 层级变化说明有其它线程抢先增加了一层索引,则退出
 71                     break;
 72                 HeadIndex<K,V> newh = h;
 73                 Node<K,V> oldbase = h.node;
 74                 //建立新一层的索引头节点newh
 75                 for (int j = oldLevel+1; j <= level; ++j)
 76                     newh = new HeadIndex<K,V>(oldbase, newh, idxs[j], j);
 77                 if (casHead(h, newh)) { //更新head指向最新的顶层索引头节点
 78                     h = newh;
 79                     idx = idxs[level = oldLevel];
 80                     break;
 81                 }
 82             }
 83         }
 84         
 85         //上面已经建立了自下而上的索引节点数组
 86         //找到索引节点的插入点并拼接进去,这里至上而下建立每一层对应的索引节点
 87         splice: for (int insertionLevel = level;;) { //自旋,level是需要插入索引的最高层级
 88             int j = h.level; //j是当前索引的最高层级
 89             //从最高层索引的头节点开始
 90             for (Index<K,V> q = h, r = q.right, t = idx;;) {
 91                 if (q == null || t == null) //遍历到头了,退出
 92                     break splice;
 93                 if (r != null) { //右边还有节点
 94                     Node<K,V> n = r.node;
 95                     // compare before deletion check avoids needing recheck
 96                     int c = cpr(cmp, key, n.key);
 97                     if (n.value == null) { //该节点已经被逻辑删除
 98                         if (!q.unlink(r)) //辅助删除节点
 99                             break;        //删除节点失败重新开始
100                         r = q.right;
101                         continue;    //删除成功,跳过该节点继续遍历
102                     }
103                     if (c > 0) { //当前节点比新key小,则还要继续往右找
104                         q = r;
105                         r = r.right;
106                         continue;
107                     }
108                 }
109                 //到这里说明已经到最右边了,或者当前索引节点大于新key
110                 if (j == insertionLevel) {    //是需要插入索引的层级
111                     if (!q.link(r, t)) //尝试将新索引节点插入该位置
112                         break; // 失败重试
113                     if (t.node.value == null) { //刚进入就被删除了
114                         findNode(key);            //辅助删除
115                         break splice;
116                     }
117                     if (--insertionLevel == 0)
118                         break splice;    //当前层级已经到底了
119                 }
120                 //向下移动层级
121                 if (--j >= insertionLevel && j < level)
122                     t = t.down;
123                 q = q.down;
124                 r = q.right;
125             }
126         }
127     }
128     return null;
129 } 
View Code

 doPut的实现代码虽然长了一点,但是并不难看懂,doPut的第三个参数onlyIfAbsent表示是否只在不存在相应的key时才插入即对替换操作的行为规定,doPut的逻辑由两个大的部分组成:1. 在基层链表上找到合适的位置插入key-value构成的目标节点,  2. 随机决定是否需要建立索引及其层次,如果要则建立相应的索引节点 。其中第二部分建立索引的过程还可以再次分为两个部分:1). 建立自下而上上的索引节点实例数组,2). 自上而下的真正将对应的索引节点插入到每一层相应的位置。注意替换键值的操作并不会触发建立索引的逻辑。

假设我在以自然排序的初始为空Map的情况下,首次插入一个key =5,value = 1,那么会变成什么样呢?第一部分中findPredecessor返回初始的虚拟节点,其next为null,直接建立新节点链接到虚拟节点的后面,再决定是否需要建立索引,假设不需要建立索引,则此时的结构如下:

虚拟节点仍然存在,并且指向新加入的节点(key =5,value = 1),此时我们再插入一个key =3,value = 2的键值对,findPredecessor还是返回虚拟节点,其next指向(5,1)节点,发现其比目标3大,所以将其插入到节点(5,1)的前面,若随机决定需要建立索引,则最终的结构如下:

由此可见,ConcurrentSkipListMap的跳表实现,存在一个虚拟节点作为基层连接的头节点,所以各层索引的headIndex都是指向该虚拟节点的。

remove(key)---删除键操作

1 public V remove(Object key) {
2     return doRemove(key, null);
3 }

 可见removet操作由doRemove实现,下面是其源码:

 1 //删除键值对
 2 final V doRemove(Object key, Object value) {
 3     if (key == null)
 4         throw new NullPointerException();
 5     Comparator<? super K> cmp = comparator;
 6     outer: for (;;) {
 7         //从通过索引拿到的第一个比key小的基层节点b开始
 8         for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
 9             Object v; int c;
10             if (n == null) //已经到头了,退出吧
11                 break outer;
12             Node<K,V> f = n.next;
13             if (n != b.next)    //不一致的读,说明链表有变化,重新开始
14                 break;
15             if ((v = n.value) == null) { // 下一个节点n已经被标记要删除
16                 n.helpDelete(b, f);        //辅助删除节点n,然后重新开始
17                 break;
18             }
19             if (b.value == null || v == n) //节点b已经被删除,重新开始
20                 break;
21             if ((c = cpr(cmp, key, n.key)) < 0)
22                 break outer; //当前节点比目标key大,说明不可能会存在相应的节点了,退出
23             if (c > 0) { //目标key比当前r对应的节点大,继续向右寻找
24                 b = n;
25                 n = f;
26                 continue;
27             }
28             //找到了对应的key了,但是值已经变了,退出了
29             if (value != null && !value.equals(v))
30                 break outer;
31             if (!n.casValue(v, null)) //将值置空失败,说明已经被其它线程更新了值,重试
32                 break;
33             //置空成功之后,尝试链接到标记节点,成功则继续尝试将b链接到被删除节点的下一个节点,真正进行物理解链
34             if (!n.appendMarker(f) || !b.casNext(n, f))
35                 findNode(key);                  // 失败了,通过findNode辅助
36             else {
37                 //appendMarker, b.casNext(n, f)都成功了,说明已经移除了基层节点
38                 findPredecessor(key, cmp);      // 该方法会移除相应的索引节点
39                 if (head.right == null)
40                     tryReduceLevel();    //如果最高层头索引节点没有右节点,则需要将无效的最高层移除
41             }
42             @SuppressWarnings("unchecked") V vv = (V)v;
43             return vv;    //返回被删除的key对应的值
44         }
45     }
46     return null;
47 } 
View Code

 remove的逻辑其实也很简单,概括下来,删除一个key其实它由三个步骤组成:1. 先将其键值value置为null,相当于标记声明该值即将被删除,2. 将节点指向一个标记节点,3. 修改前驱节点的next指向真正的后继节点即拆链建立新连接。

假设有如下一段链接:

假如我们要删除节点n对应的键值对,那么首先将n节点的value置为null,然后将其next指向标记节点:

标记节点的value指向该标记节点本身,其next指向节点n的next,最后再将b的next指向f,完成整个过程:

当然除了上述三个具体的删除节点过程,在remove的时候还需要删除节点对应的索引节点,如果有的话,该步骤通过调用findPredecessor辅助完成,最后如果最高层索引除了头节点没有任何其他索引节点的话,还需要执行tryReduceLevel将最高层索引层清除,达到收缩索引的目的,这样有利于减少无效索引层造成的性能和内存消耗。

另外doRemove支持只有当value等于其原值时才移除的设计,即第二个参数value不为空时,需要确保value没有被更改,否则不予删除。ConcurrentSkipListMap.remove(key, value)就是这样的逻辑。

ConcurrentSkipListMap的这些操作实现过程中存在很多相同的代码,例如都有一些查找过程,辅助删除节点的过程,根据JDK DOC,说是因为不能很好的分解这些代码,同时不能为了同时跟踪多个字段(前驱,后继,value值)快照而建立一些额外的小对象从而会增加搜索代码的复杂度和GC开销,所以这些代码相互交错在了一起。

size()方法

 1 //找到基层链表第一个有效实体节点     
 2 final Node<K,V> findFirst() {
 3     for (Node<K,V> b, n;;) {
 4         //因为head是一个虚拟节点,所以直接取其next
 5         if ((n = (b = head.node).next) == null)
 6             return null;  //队列为空,返回null
 7         if (n.value != null) //是有效节点,返回
 8             return n;
 9         n.helpDelete(b, n.next); //是已经标记删除的节点,辅助删除之后继续右下走
10     }
11 }
12 
13 public int size() {
14     long count = 0;
15     for (Node<K,V> n = findFirst(); n != null; n = n.next) {
16         if (n.getValidValue() != null)
17             ++count;
18     }
View Code

 ConcurrentSkipListMap的size方法根据findFirst返回的基层链表上第一个有效节点,开始依次往后统计不是已经标记删除的节点,不是标记节点,也不是虚拟节点的其他有效实体节点的个数。

其他一些方法

有了上面关于ConcurrentSkipListMap的一些方法的理解,理解其他一些方法就很容易了。首先是实现的一批NavigableMap的接口方法:

  • lowerEntry、floorEntry、ceilingEntry 和 higherEntry 分别返回刚好小于、小于等于、大于等于、大于给定键的键值对的 Map.Entry 对象,如果没有这样的键,则返回null。
  • lowerKey、floorKey、ceilingKey 和 higherKey 与其Entry方法对应,但只分别返回刚好小于、小于等于、大于等于、大于给定键的键值对的键,如果没有这样的键,则返回null。
  • firstEntry、pollFirstEntry、lastEntry 和 pollLastEntry 方法,它们返回并且可以同时移除(带poll的相应方法)最小和最大的键值对,如果存在的话,否则返回 null。
  • keySet、navigableKeySet这两个方法是相同的语义,都是返回该Map所有键的NavigableSet类型视图。该返回视图集合的迭代器按升序返回所有键,并且返回视图集合的可拆分迭代器也是升序并且具有CONCURRENT,NONNULL,SORTED ,ORDERED特性,通过Spliterator.getComparator()可以获取被迭代的视图的比较器,这两种迭代器都是弱一致性的。最重要的是,该返回Set与原Map是相互关联的,即改变任意一方都会反映到另一方的结构中。该返回Set支持通过Iterator.remove, Set.remove, removeAll, retainAll, clear删除元素,原Map中对应的键值也会被删除。该视图不支持add/addAll操作 。该方法通过内部类KeySet辅助实现。
  • descendingKeySet返回该Map所有键的NavigableSet类型视图,与keySet、navigableKeySet不同的是,该视图的迭代器按降序返回所有键,其他特性(上面橙色斜体部分)都和keySet、navigableKeySet一致,该方法通过内部类SubMap辅助实现。
  • Collection<V> values()返回该Map所有值的Collection视图,该返回视图集合的迭代器是按其相关键的升序返回对应的值,除了它是对值视图的返回外,其他特性(上面橙色斜体部分)都和keySet、navigableKeySet一致,该方法通过内部类Values辅助实现。
  • descendingMap按降序返回该Map所有键值对的ConcurrentNavigableMap视图,其他特性(上面橙色斜体部分)都和keySet、navigableKeySet一致,该方法通过内部类SubMap辅助实现。
  • headMap、tailMap、subMap返回具有受限键范围的子ConcurrentNavigableMap视图,其他特性(上面橙色斜体部分)都和keySet、navigableKeySet一致,该方法通过内部类SubMap辅助实现。

其次就是实现和重写的一批ConcurrentMap接口的方法:

  • getOrDefault 返回指定键映射到的值,如果此映射不包含键的映射,则返回defaultValue。
  • putIfAbsent(K key, V value); 如果当前Map不存在指定的键,就将该键值对加入Map,并返回原来的值,否则直接返回存在的键值。
  • remove(Object key, Object value); 仅当当前Map存在给定的键和值,才删除该键值对,并返回true,否则返回false。
  • replace(K key, V oldValue, V newValue); 仅当当前Map存在给定的键和值,才替换键的值,并返回true,否则返回false。
  • replace(K key, V value);仅当当前Map存在给定的键时,才替换键的值,并返回原来的值,否则返回null
  • replaceAll(BiFunction<? super K, ? super V, ? extends V> function);遍历每一对键值对,根据其key和value和指定的函数计算出值作为其key对应的新值
  • V computeIfAbsent(K key, Function mappingFunction);如果指定的键尚未与值关联(或映射为null),则尝试使用给定的映射函数和key计算其值,并将其加入Map,除非为null。返回已经存在的相应键值或计算得出的键值,如果计算得出的键值为null则返回null。
  • V computeIfPresent(K key,BiFunction<? super K, ? super V, ? extends V> remappingFunction) 如果指定键的值存在且非null,则尝试根据给定键及其当前原键值计算出新键值。若新值不为null则替换相应的键值,并返回新值;否则移除原键值对,返回null
  • V compute(K key,BiFunction<? super K, ? super V, ? extends V> remappingFunction);尝试根据指定的键及其值计算出新的键值,新值不为null则替换原值,返回与指定键关联的新值,否则原值不为null,新值为null,移除原键值对,返回null
  • V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction)尝试如果指定的键尚未与值关联或与null关联,则将其与给定的非null值value关联,并返回非null的value。否则,将关联值替换为根据原值和给定value通过给定函数计算的结果,并返回该新值,如果结果为空,则删除关联值返回null。

ConcurrentSkipListMap重新了equals方法,要两个map的所有键值对都一样才返回true。

迭代器、可拆分迭代器

ConcurrentSkipListMap作为一种Map不能直接迭代(包括可拆分迭代),只能通过它的Key视图或者Entry视图进行迭代(包括拆分迭代)操作,例如:map.navigableKeySet().iterator(); map.descendingKeySet().spliterator(); 关于迭代器的特性其实上面的橙色字体已经说明了。两种迭代器都是弱一致性的,并且是有序的,其迭代顺序与被迭代视图的顺序一致,通过迭代器修改视图将会反映到源ConcurrentSkipListMap实例中,反之亦然。

可拆分迭代器是默认的Spliterators.IteratorSpliterator实现的,与ArrayBlockingQueue的可拆分迭代器一致,所以它的顶层Spliterator迭代器其实还是调用的Iterator的实现,即hasNext + next的方式进行迭代,但是拆分后的迭代器则是ArraySpliterator实例,它是直接根据数组下标进行迭代的。IteratorSpliterator只会当数据源大小大于1024的时候才有效,否则仅仅是将原数据源全部转移成ArraySpliterator,因为拆分迭代器的原理就是将原迭代器的数据源分出一部分(具体如何拆分,不同的实现不同)产生新的迭代器,被拆分出来的迭代器的数据将会从原迭代器中移除,拆分后的迭代器可以继续拆分,直到数据量小到不能再次拆分为止,trySplit将会返回null。这里的IteratorSpliterator实现就是每一次拆分一半,直到只剩下1个元素的时候将无法再被拆分。

这里的顶层可拆分迭代器IteratorSpliterator与源ConcurrentSkipListMap是相互影响的,通过迭代器修改视图将会反映到源ConcurrentSkipListMap实例中,反之亦然。但是拆分后的迭代器ArraySpliterator是在拆分的时候将顶层迭代器的数据的一半直接拷贝到一个新的数组中,在迭代的时候直接通过数组下标访问该数组,所以这已经脱离了源数据ConcurrentSkipListMap,其内部元素并不会随着队列的变化而被更新。

ConcurrentSkipListSet

终于将ConcurrentSkipListMap弄明白了,现在来看ConcurrentSkipListSet就一目了然了,因为ConcurrentSkipListSet在内部就是通过一个ConcurrentSkipListMap实例实现其所有操作的:

 1 public class ConcurrentSkipListSet<E>
 2     extends AbstractSet<E>
 3     implements NavigableSet<E>, Cloneable, java.io.Serializable {     
 4      
 5     //借助ConcurrentNavigableMap实现
 6     private final ConcurrentNavigableMap<E,Object> m;
 7 
 8     //无参构造方法
 9     public ConcurrentSkipListSet() {
10         m = new ConcurrentSkipListMap<E,Object>();
11     }
12     
13     ......

构造实例的时候就初始化了一个ConcurrentSkipListMap实例,下面是它的一部分实现方法:

 1 public boolean add(E e) {
 2     return m.putIfAbsent(e, Boolean.TRUE) == null;
 3 }
 4 
 5 public boolean remove(Object o) {
 6     return m.remove(o, Boolean.TRUE);
 7 }
 8 
 9 public E lower(E e) {
10     return m.lowerKey(e);
11 }
12 
13 public E first() {
14     return m.firstKey();
15 }
16 
17 public NavigableSet<E> descendingSet() {
18     return new ConcurrentSkipListSet<E>(m.descendingMap());
19 }
20 
21 public NavigableSet<E> subSet(E fromElement,
22                               boolean fromInclusive,
23                               E toElement,
24                               boolean toInclusive) {
25     return new ConcurrentSkipListSet<E>
26         (m.subMap(fromElement, fromInclusive,
27                   toElement,   toInclusive));
28 }
29 public Iterator<E> iterator() {
30     return m.navigableKeySet().iterator();
31 }
32 public Spliterator<E> spliterator() {
33     if (m instanceof ConcurrentSkipListMap)
34         return ((ConcurrentSkipListMap<E,?>)m).keySpliterator();
35     else
36         return (Spliterator<E>)((ConcurrentSkipListMap.SubMap<E,?>)m).keyIterator();
37 }
38 public Iterator<E> descendingIterator() {
39     return m.descendingKeySet().iterator();
40 }
41 ....
View Code

可以看见它的所有实现确实都是借助ConcurrentSkipListMap实现的,作为一个Set只需要存储Key,所以它在ConcurrentSkipListMap的value中存储了布尔值,这样可以减少内存占用。它实现的NavigableSet接口与NavigableMap一样都是一种支持快速查找和返回子视图的导航接口,所以ConcurrentSkipListSet同样借助了ConcurrentSkipListMap的相关方法实现了NavigableSet的接口方法,例如:

lower,floor,ceiling,higher分别返回刚好小于、小于等于、大于等于、大于给定元素的元素,如果没有则返回null。

first,pollFirst,last,pollLast返回并且可以同时移除(带poll的相应方法)最小和最大的元素,如果存在的话,否则返回 null。

descendingSet返回降序的所有元素视图,subSet,headSet,tailSet返回具有受限范围的子NavigableSet视图。

ConcurrentSkipListSet同样重写了equals,要两个Set的元素都一样才返回true。

由于ConcurrentSkipListSet的迭代器都是使用的ConcurrentSkipListMap实现,所以它的迭代器(包括可拆分迭代器)与ConcurrentSkipListMap的迭代器拥有完全一样的特性,就不一一多说了。要了解的就看上面关于迭代器、可拆分迭代器的介绍。

总结

ConcurrentSkipListMap是一种基于跳表实现的变种,通过空间换时间的多层索引算法实现比起hashMap,红黑树实现简单太多了,它是一种有序的能够实现高效插入,删除,更新,搜索的基于链表结构的无锁线程安全Map,它作为NavigableMap的实现类能够根据指定的key获取特定范围的子视图,甚至是降序的,这在某些时候是非常有用的比如从redis中获取部分缓存数据,所以据说redis内部实现也有类似的实现。ConcurrentSkipListSet完全是借助ConcurrentSkipListMap实现的,ConcurrentSkipListSet只是将所有键值对的值用一个布尔型填充,所以他们两个除了一个是Set,一个是Map,其内部特性都是一样的。

posted @ 2019-07-06 15:32  莫待樱开春来踏雪觅芳踪  阅读(561)  评论(0编辑  收藏  举报