随机数据结构:跳表(SkipList)

在JDK的并发包中,除了常用的哈希表外,还有一种有趣的数据结构—跳表。跳表是一种可以用来快速查找的数据结构,有点类似于平衡树。它们的相同点都是可以对元素进行快速的查找。但有一个很重要的差别:对平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整。而对跳表的插入和删除只需要对整个数据结构的局部进行操作即可。这样导致的结果就是,在高并发的情况下,你会需要一个全局锁来保证整个平衡树的线程安全,对于跳表,你只需要局部加锁即可。这样,就导致了跳表在高并发的情况下拥有更好的性能。

  跳表的本质是同时维护了多个链表,并且链表是分层的,如下图所示:

如上图所示,最低层的链表维护了跳表内所有的元素,每上面一层链表都是下面一层的子集,一个元素插入哪些层是完全随机的。因此,跳表还是有可能会有很糟糕的结构出现的概率的。但是在实际中,跳表的表现是非常好的。

还可以看出跳表内的所有链表的元素都是排序的,可以得出,跳表是一种空间换时间的算法。

跳表的查找方式:

  跳表查找时,可以从顶级链表开始找,一旦发现被查找的元素大于当前链表中的取值,就会转入下一层开始查找。也就是说,在查找中,搜索是跳跃式的。

  举例说明:从上述跳表结构中查找元素7,查找从顶层的头部索引节点开始。由于顶层的元素少,可以快速的跳跃那些小于7的元素。很快,查找过程就能到元素6,这时转入第二层,由于元素8大于7,故无法在第二层找到元素7,所以转入最低层开始查找,并且很快就能根据元素6找到元素7,这个查找元素的过程要比从元素1开始查找逐个搜索快很多,整个过程如下图红线所示:

 

在JDK中采用跳表结构的并发容器有:ConcurrentSkipListMapConcurrentSkipListSet

ConcurrentSkipListMap:

ConcurrentSkipListMap和ConcurrentHashMap的不同之处不仅仅在于底层结构一个是跳表结构,一个是哈希算法结构;还有一个不同之处在于,哈希不会保存元素的顺序,而跳表内所有的元素都是有序的。因此,遍历跳表时,会得到一个有序的结果。所以,当你的应用需要有序性时,跳表就是你的不二选择。

先举例看看跳表Map(ConcurrentSkipListMap)的简单使用:

1 public static void main(String[] args){
2         Map<Integer,Integer> map = new ConcurrentSkipListMap<Integer, Integer>();
3         for (int i = 0;i < 10;i++){
4             map.put(i,i);
5         }
6         for (Map.Entry<Integer,Integer> entry : map.entrySet()){
7             System.out.println(entry.getKey());
8         }
9     }

输出结果:

0
1
2
3
4
5
6
7
8
9

可以从输出结果看出,ConcurrentSkipListMap遍历输出是有序的。,并且使用的方式跟哈希Map的用法一致。

接下来通过源码来探究ConcurrentSkipListMap的底层实现:

首先是Node

 1 static final class Node<K,V> {
 2         final K key;
 3         volatile Object value;
 4         volatile Node<K,V> next;
 5 
 6         Node(K key, Object value, Node<K,V> next) {
 7             this.key = key;
 8             this.value = value;
 9             this.next = next;
10         }

可以看出Node是由静态内部类实现的,一个Node就是一个节点,里面含有三个元素,key和value(就是Map的key和value),每个Node会指向下一个Node,next就是干这事的。value和next是关键字volatile修饰的,这保证了在并发环境下,线程对于value和next的可见性。对Node的操作,都是使用的CAS方法:

1       boolean casValue(Object cmp, Object val) {
2             return UNSAFE.compareAndSwapObject(this, valueOffset, cmp, val);
3         }
4 
5         boolean casNext(Node<K,V> cmp, Node<K,V> val) {
6             return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
7         }

casValue()是用来设置value的值的方法,casNext()则是用来设置next的字段。

数据结构Index:

  index表示索引,看它的内部实现:

        final Node<K,V> node;
        final Index<K,V> down;
        volatile Index<K,V> right;    

上述时Index的类的变量定义,Index是一个ConcurrentSkipListMap的静态内部类,它包含了Node,还有向下和向右的引用。整个跳表就是根据Index进行全网的组织的。

此外,对于每一层的表头,还需要记录当前处于哪一层。所以,还有一个数据结构HeadIndex,表示链表头部的第一个Index,它继承于Index:

1 static final class HeadIndex<K,V> extends Index<K,V> {
2         final int level;
3         HeadIndex(Node<K,V> node, Index<K,V> down, Index<K,V> right, int level) {
4             super(node, down, right);
5             this.level = level;
6         }
7     }

level表示第几层,这样一个index包含有Node,向下的引用,向右的引用,第几层,通过这些条件就能准确的定位元素的位置了。对于跳表的所有操作,就是组织好这些Index之间的连接关系。

跳表的特点:

  ❤ 跳表的查询时间复杂度是O(log n);

  ❤ 随机算法;

  ❤ 跳表内所有链表的元素都是排序的;

  ❤ 采用CAS算法,保证线程安全。

参考:《Java高并发程序设计》 葛一鸣 郭超 编著:

作者:Joe
努力了的才叫梦想,不努力的就是空想,努力并且坚持下去,毕竟这是我相信的力量
原文地址:https://www.cnblogs.com/Joe-Go/p/9789842.html