zoukankan      html  css  js  c++  java
  • 随机数据结构:跳表(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
    努力了的才叫梦想,不努力的就是空想,努力并且坚持下去,毕竟这是我相信的力量
  • 相关阅读:
    Java基础面试题总结
    mysql面试题总结
    记7.9面试题
    深入理解Java虚拟机学习笔记(三)-----类文件结构/虚拟机类加载机制
    jvm类加载子系统
    多线程与高并发基础三
    多线程与高并发基础二
    多线程与高并发基础一
    多线程与高并发笔记一
    Unknown initial character set index '255' received from server. Initial client character set can be forced via the 'characterEncoding' property.
  • 原文地址:https://www.cnblogs.com/Joe-Go/p/9789842.html
Copyright © 2011-2022 走看看