JDK源码阅读计划(Day13) ConcurrentSkipListMap & 跳表学习

跳表

在这里插入图片描述
上图可以看到,一个有序单链表,查找某元素的平均时间复杂度为O(n)

跳表本质上是在有序链表上建立多层索引,以实现二分查找。以空间换时间的思想,实现增删查改平均时间复杂度为O(lgn)

而skipList的结构可能有2种:

第一种是每个结点会指向向右和向下的结点,像ConcurrentSkipListMap就是这么设计的

  /**
     * Index nodes represent the levels of the skip list.
     */
    // 跳表索引,存储右侧跟下侧的索引,组成一个十字链表
    static final class Index<K,V> {
        final Node<K,V> node;  // currently, never detached
        
        final Index<K,V> down;
        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;
        }
    }

下图就是一个好例子:
JDK源码阅读计划(Day13) ConcurrentSkipListMap & 跳表学习_第1张图片
我在面试阿里云的时候笔试题就是手写跳表,当时我也打算是这么设计结点的结构。但面试官提到,这么做的话其实会非常浪费空间。首先最底层的原始链表都是没有向下结点,最后一个结点也没有向右结点,虽然next和right都是引用,但是未免还是有点浪费…更重要的是,假设跳表有10层,那么假设要插入一个新的节点,可能要在10层(不一定10层,这个是随机的)中都添加这个节点,虽然每一层next和right可能不同,但还是会造成数据冗余对吧。

因此另外一种定义结点的方式是这样的:

class Node{
      int data;
      Node[] forward; //forward[i]表示处于第i层的结点Node的下一个结点
                      //随机高度数 k决定节点的高度 h,节点的高度 h决定节点中forword的长度;}

每一个结点有一个结点数组,可以指向其每一层指向的结点。假设这个Node分布在10层中,那么forward长度就是10,forward[0]是其在第一层的后继节点…以此类推

那么每次插入一个新节点只需要new 一个Node,forward本质上也是一个数组的引用。那么总比上面那种可能要new 10次要强吧?

那我们以第二种定义方式继续讨论:

在这里插入图片描述

假设要找元素15

1.从head结点开始,从最顶层的1开始找,下一个结点为8,跳到8
2.下一个节点为18,从8的下一层开始寻找
3.在最底层依次经过8,10,13最后找到15

可以把除了最底层链表的结点都看作是索引层,通过额外引入索引节点,达到查询时候忽略某些节点的效果,本质上是以空间换时间

这里参考跳表实现尝试以这种定义方式手写一个跳表轮子加深理解,源代码在这skipList

  • find

与第一种定义的方法不同的是,这种方法必须要在原始链表中查找元素

public Node find(int val) {

        //begin with head node
        Node p = head;
        for (int l = levelCount - 1; l >= 0; l--) {
            while (p.forward[l] != null && p.forward[l].data < val) {
                p = p.forward[l];
            }
        }
        if(p.forward[0]!=null&&p.forward[0].data==val){
            return p.forward[0];
        }else{
            return null;
        }
    }

结合上图来理解,从头节点指向的最顶层(第l层)结点开始,如果其后继节点p.forward[l]小于val,则继续向右。如果大于val的话,就while循环结束,进入到下一层继续找

一直找到最后一层,直到p的后继节点就是我们要找的value结点。否则返回null

  • insert

大意是每插入一个新结点,随机生成一个其出现的最高Level,然后只有在小于level的层的链表才能插入这个新结点

public void insert(int val) {
        Node p = head;
        //如果是第一个插入的结点那肯定是第一层
        int level = p.forward[0] == null ? 1 : randomLevel();

        //只允许每次新增加一层
        if (level > levelCount) {
            level = ++levelCount;
        }

        //最高处于第level层的结点应该创建一个指向大小为level的Node数组引用,用来记录各层的后继节点
        Node newNode = new Node(level);
        newNode.data = val;

        for (int l = levelCount - 1; l >= 0; l--) {
            while (p.forward[l] != null && p.forward[l].data < val) {
                p = p.forward[l];
            }
            //只有在level>l的层才能够插入新结点
            if (level > l) {
                if (p.forward[l] == null) {
                    p.forward[l] = newNode;
                } else {
                    Node next = p.forward[l];
                    p.forward[l] = newNode;
                    newNode.forward[l] = next;
                }
            }
        }
    }
  • delete

大意是开一个数组记录每一层可能将要删除结点的前继结点
然后如果原始链表层上找到了这个要删除的元素
然后遍历所有层,如果数组中该层对应节点的后一个节点就是要删除的元素,就把它删除

 public void delete(int val) {
        //开一个levelCount大小的数组,记录每一层可能要删除的结点的前一个结点
        Node[] toDeleteNode = new Node[levelCount];
        Node p = head;
        for (int l = levelCount - 1; l >= 0; l--) {
            while (p.forward[l] != null && p.forward[l].data < val) {
                p = p.forward[l];
            }
            toDeleteNode[l] = p;
        }

        //当仅当原始链表上的p的后继节点等于val,即找到之后才返回
        if(p.forward[0]!=null&&p.forward[0].data==val){
            for (int l = levelCount - 1; l >= 0; l--) {
                if (toDeleteNode[l].forward[l] != null && toDeleteNode[l].forward[l].data == val) {
                    toDeleteNode[l].forward[l] = toDeleteNode[l].forward[l].forward[l];
                }
            }
        }
    }

ConcurrentSkipListMap

提供了一种线程安全有序的Map,内部实现是SkipList(跳表)理论上能够实现O(lgn)时间复杂度内完成增删查改操作。
JDK源码阅读计划(Day13) ConcurrentSkipListMap & 跳表学习_第2张图片

重要成员

 /**
     * The comparator used to maintain order in this map, or null if
     * using natural ordering.  (Non-private to simplify access in
     * nested classes.)
     * @serial
     */
    final Comparator<? super K> comparator; // 外部比较器
    
    /** Lazily initialized topmost index of the skiplist. */
    private transient Index<K,V> head;  // 跳表索引起点,需要从这里开始搜索元素
    
    /** Lazily initialized element count */
    private transient LongAdder adder;  // 元素计数
   
	//以下皆是懒加载
    /** Lazily initialized key set */
    private transient KeySet<K,V> keySet;
    /** Lazily initialized values collection */
    private transient Values<K,V> values;
    /** Lazily initialized entry set */
    private transient EntrySet<K,V> entrySet;
    
    
    /** Lazily initialized descending map */
    private transient SubMap<K,V> descendingMap;    // 【逆序】Map

两种结点

通常最后一层的原始链表使用Node来表示的,结点之间以next指针相连
而除了最后一层的链表都是索引层,只有向下和向右结点

  • Node
// 跳表元素,存储键值对的引用,组成一个单链表
    static final class Node<K,V> {
        final K key; // currently, never detached
        V val;
        Node<K,V> next;
        Node(K key, V value, Node<K,V> next) {
            this.key = key;
            this.val = value;
            this.next = next;
        }
    }
  • Index
// 跳表索引,存储右侧跟下侧的索引,组成一个十字链表
    static final class Index<K,V> {
        final Node<K,V> node;  // currently, never detached
        
        final Index<K,V> down;
        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;
        }
    }

貌似我看的JDK11代码和网上的源码解读中CSLM的代码增删查改操作都有很大的不同,而删除了HeadIndex这种类型,而且其关于跳表的操作非常非常的复杂…代码很长有兴趣可以自己看看源码
本人水平有限就不贴代码了…但是通篇扫下去,对跳表结构的修改都是用CAS操作来完成,也没有用到synchroinze关键字来保证线程安全。

其实可以趁这个机会学习下lock-free skip list是怎么实现的,这又是一个很大的topic了哎,但关键还是用CAS操作来保证线程安全,以后有时间再看看redis的skipList源码和levelDB的skipList源码。

emmm瞄了一眼
redis中的skipList结点大概是按照本文第二种方法定义的,增删查改也和我写的简单轮子相似,代码量少了很多呢

typedef struct zskiplistNode {     
    sds ele;                              //数据域
    double score;                         //分值 
    struct zskiplistNode *backward;       //后向指针,使得跳表第一层组织为双向链表
    struct zskiplistLevel {               //每一个结点的层级
        struct zskiplistNode *forward;    //某一层的前向结点
        unsigned int span;                //某一层距离下一个结点的跨度
    } level[];                            //level本身是一个柔性数组,最大值为32,由 ZSKIPLIST_MAXLEVEL 定义

最后讲讲varHandle吧

变量句柄

参考 https://blog.csdn.net/sench_z/article/details/79793741

在JDK9之后以及开始逐渐把Unsafe类方法等提供原子操作的方法转换为用varhandle来提供,VarHandle 的出现替代了 java.util.concurrent.atomic 和 sun.misc.Unsafe 的部分操作。并且提供了一系列标准的内存屏障操作,用于更加细粒度的控制内存排序。在安全性、可用性、性能上都要优于现有的API。

// VarHandle mechanics
    private static final VarHandle HEAD;
    private static final VarHandle ADDER;
    private static final VarHandle NEXT;
    private static final VarHandle VAL;
    private static final VarHandle RIGHT;
    static {
        try {
            MethodHandles.Lookup l = MethodHandles.lookup();
            HEAD = l.findVarHandle(ConcurrentSkipListMap.class, "head", Index.class);
            ADDER = l.findVarHandle(ConcurrentSkipListMap.class, "adder", LongAdder.class);
            NEXT = l.findVarHandle(Node.class, "next", Node.class);
            VAL = l.findVarHandle(Node.class, "val", Object.class);
            RIGHT = l.findVarHandle(Index.class, "right", Index.class);
        } catch(ReflectiveOperationException e) {
            throw new ExceptionInInitializerError(e);
        }
    }

在源码中的的CAS方法都是由上面的变量句柄提供如

RIGHT.compareAndSet(q, r, r.right);

除此之外Varhandle方法还会提供内存屏障操作如:

  @ForceInline
    public static void fullFence() {
        UNSAFE.fullFence();
    }

  @ForceInline
    public static void acquireFence() {
        UNSAFE.loadFence();
    }

内存屏障的核心语义就是禁止指令重排,保证happens before

.

ref

https://segmentfault.com/a/1190000016168566?utm_source=tag-newest
https://blog.csdn.net/sench_z/article/details/79793741

你可能感兴趣的:(JAVA)