简单高效的跳跃表 ConcurrentSkipListMap

1,178 阅读11分钟

1、简介

看了TreeMap、HashMap源码的,或者手写实现红黑树、平衡二叉树的,感觉插入和删除手写太难了;我逻辑理解也不是很顺畅,插入基本搞清楚了,但是删除,我就呵呵了

那么,有没有一种性能和红黑树或者平衡二叉树不相上下,且又很好实现的数据结构,答案是肯定,我知道的至少有一种 跳表;不过也有缺点,看实现方式(我找的资料都是插入时,对刚插入的数据随机处理索引),不一定稳定

什么叫跳表呢? 跳表是一个随机化的有序链表。跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。跳表不仅能提高搜索性能,同时也可以提高插入和删除操作的性能。

它采用随机技术决定链表中哪些节点应增加向前指针以及在该节点中应增加多少个指针。 采用这种随机技术,跳表中的搜索、插入、删除操作的时间均为O(logn),然而,最坏情况下时间复杂性却变成O(n)。 跳表的结构大致如下图:(来源百度百科

2、ConcurrentSkipListMap源码分析

2.1 数据节点

        final K key;
        volatile Object value;
        volatile Node<K,V> next;

节点数据包含,key,value和下个数据;也就是采用单链表的结构存储实际数据

具体的代码操作就不贴出了,大概有以下内容:

  1. 使用sun.misc.Unsafe类来实现原子操作;配合volatile关键+自旋可以实现线程安全操作
  2. 实现了值改变原子操作,后驱节点变化原子操作,当前next节点删除的线程安全操作方法

2.2 普通索引节点

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

包含对应数据,下方索引节点,右边索引节点;

  1. 一个特殊的单链表,和下方索引节点的数据节点相同;
  2. 实现右边索引节点的原子操作

2.3 头索引节点 HeadIndex<K,V>

static final class HeadIndex<K,V> extends Index<K,V> {
        final int level;
        HeadIndex(Node<K,V> node, Index<K,V> down, Index<K,V> right, int level) {
            super(node, down, right);
            this.level = level;
        }
    }

继承了普通索引节点,增加了索引级别,从1开始;

2.4 主要成员变量

    static final Object BASE_HEADER = new Object();

    private transient volatile HeadIndex<K,V> head;

    final Comparator<? super K> comparator;

head 索引节点,comparator比较器;特点

  1. 头索引节点,存储的值对为 null-BASE_HEADER;数据节点第一个值为BASE_HEADER
  2. 头索引节点的变化实现了原子操作
  3. head值为最高索引级别(level最大)的第一个索引节点
  4. 存储key值实现了Comparator接口,或者提供了比较器comparator,不然比较时抛出异常

2.5 查找前驱节点

   private Node<K,V> findPredecessor(Object key, Comparator<? super K> cmp) {
        if (key == null)
            throw new NullPointerException(); // don't postpone errors
        for (;;) {
            for (Index<K,V> q = head, r = q.right, d;;) {
                if (r != null) {
                    Node<K,V> n = r.node;
                    K k = n.key;
                    if (n.value == null) {
                        if (!q.unlink(r))
                            break;
                        r = q.right;
                        continue;
                    }
                    if (cpr(cmp, key, k) > 0) {
                        q = r;
                        r = r.right;
                        continue;
                    }
                }
                if ((d = q.down) == null)
                    return q.node;
                q = d;
                r = d.right;
            }
        }
    }
  1. 首先判断key值,为空,抛出异常
  2. 采用双层for循环;在原子性操作去除无效值失败时,内循环重新开始,相当于自旋操作,保证线程同步
  3. 内层循环采用right索引的值域比较大小:
  • right索引不为空,首先如果rigth索引的值域无效(值节点中value为空),则原子操作(unlink方法)链接q节点和right索引的rigt索引,失败重新循环,成功,索引向右移动
  • right索引不为空,如果right索引的值-key 比当前key小(cpr(cmp, key, k), key大,返回 > 0),则继续往右移动,进行下次循环
  • right索引为空,或者小,则索引向下移动
  • 如果down指针域为空,则说明,这就是在当前所有索引节点中,小于key的最大索引节点

向下移动,则是把down指针域当成next,进行链表移动

向右移动,则是把right指针域当成next,进行链表移动

2.6 查找节点 Node<K,V> findNode(Object key)

private Node<K,V> findNode(Object key) {
        if (key == null)
            throw new NullPointerException();
        Comparator<? super K> cmp = comparator;
        outer: for (;;) {
            for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
                Object v; int c;
                if (n == null)
                    break outer;
                Node<K,V> f = n.next;
                if (n != b.next) 
                    break;
                if ((v = n.value) == null) { 
                    n.helpDelete(b, f);
                    break;
                }
                if (b.value == null || v == n) 
                    break;
                if ((c = cpr(cmp, key, n.key)) == 0)
                    return n;
                if (c < 0)
                    break outer;
                b = n;
                n = f;
            }
        }
        return null;
    }
  1. key值检验有效性
  2. 双层循环;内循环处理流程
  • 如果next节点为空,则说明未找到匹配的数据,则跳出双层循环,结束流程,返回null
  • 检查数据是否发生了变化,如果发生变化,重新内循环
  • 检查数据有效性,无效时,去除节点(cas操作,有可能失败),重新循环
  • 检查b、n数据有效性,无效重新内循环
  • 比较索引值域-key 和key的大小,相当表明找到数据,返回;小于0,则不存在,跳出双层循环,返回null;大于0,继续往后找

由此可见,数据节点按照key的从小到大排列的

2.7 获取数据 get

    public V get(Object key) {
        return doGet(key);
    }
    
    private V doGet(Object key) {
        if (key == null)
            throw new NullPointerException();
        Comparator<? super K> cmp = comparator;
        outer: for (;;) {
            for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
                Object v; int c;
                if (n == null)
                    break outer;
                Node<K,V> f = n.next;
                if (n != b.next)   
                    break;
                if ((v = n.value) == null) {    
                    n.helpDelete(b, f);
                    break;
                }
                if (b.value == null || v == n)  
                    break;
                if ((c = cpr(cmp, key, n.key)) == 0) {
                    @SuppressWarnings("unchecked") V vv = (V)v;
                    return vv;
                }
                if (c < 0)
                    break outer;
                b = n;
                n = f;
            }
        }
        return null;
    }

和findNode方法基本一致,就是在下面代码中,返回值不同;一个是返回节点,一个是返回节点value值

                if ((c = cpr(cmp, key, n.key)) == 0) {
                    @SuppressWarnings("unchecked") V vv = (V)v;
                    return vv;
                }

2.8 增加、修改数据

    public V put(K key, V value) {
        if (value == null)
            throw new NullPointerException();
        return doPut(key, value, false);
    }
    
    private V doPut(K key, V value, boolean onlyIfAbsent) {
        Node<K,V> z;
        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)     
                        break;
                    if ((v = n.value) == null) {  
                        n.helpDelete(b, f);
                        break;
                    }
                    if (b.value == null || v == n) 
                        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
                    }
                }
                z = new Node<K,V>(key, value, n);
                if (!b.casNext(n, z))
                    break; 
                break outer;
            }
        }

        int rnd = ThreadLocalRandom.nextSecondarySeed();
        if ((rnd & 0x80000001) == 0) {
            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 { 
                level = max + 1; 
                @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;
                    }
                }
            }

            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;
                        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;
                        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;
    }

从上面代码可以看出,分两个大步骤:一是插入数据域节点(修改值只有这一步),二是索引层处理

插入数据:和get思路大同小异,存在一下区别

  1. 找到相同key的数据节点时,替换value值,结束整个流程
  2. 如果向后查找,当前节点next大于要插入key值,说明插入节点在当前节点和后面节点之间,原子操作链接,成功进行下一步

索引层处理

  1. 随机一个正数,如果不满足(rnd & 0x80000001) == 0,则结束
  2. 通过随机数rnd,来确定做多有多少层level
  3. 如果level层数小于现有层,则直接生成当前数据的从1到level的索引节点
  4. 如果level层大于现有层,则增加一层:生成当前数据从1到level的索引节点,并生成原有层到新的level层的头节点,头节点右值域为当前数据同层索引节点;h为新的head指针,idxs为当前插入索引节点在旧的索引层最高层
  5. 双层循环:外层循环仍是检验数据发生变化时重新处理,保证数据的线程安全
  • 如果当前插入数据代表的索引节点为空,或者是,上次循环时索引层为1,结束
  • 去除无效索引节点;成功,向右移动,继续循环,失败则从外层循环重新开始;
  • 当前插入数据key值大,继续右移,继续循环
  • 这时已经找到位置;找到位置的level和插入数据的level同层,则索引节点插入;插入为原子操作,失败,则从内层循环重新开始
  • 找到位置的level和插入数据的level同层,存在当前插入索引为无效数据,则跳出外层循环,结束
  • 找到位置的level和插入数据的level同层;插入数据,level 减1,如果此时值为0,说明索引节点全部链接成功,结束
  • 如果当前内循环时索引节点level,比插入数据level大,则内循环索引节点向下移动,继续循环
  • 如果内循环索引节点不大于插入数据level节点,则一起向下移动(在我看来,这就应该是同级移动),继续循环

2.9 移除数据 remove

    public V remove(Object key) {
        return doRemove(key, null);
    }
    final V doRemove(Object key, Object value) {
        if (key == null)
            throw new NullPointerException();
        Comparator<? super K> cmp = comparator;
        outer: for (;;) {
            for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
                Object v; int c;
                if (n == null)
                    break outer;
                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)
                    break outer;
                if (c > 0) {
                    b = n;
                    n = f;
                    continue;
                }
                /////////////////////////////////////////////// 相等
                if (value != null && !value.equals(v))
                    break outer;
                if (!n.casValue(v, null))
                    break;
                if (!n.appendMarker(f) || !b.casNext(n, f))
                    findNode(key);                  // retry via findNode
                else {
                    findPredecessor(key, cmp);      // clean index
                    if (head.right == null)
                        tryReduceLevel();
                }
                @SuppressWarnings("unchecked") V vv = (V)v;
                return vv;
            }
        }
        return null;
    }

又是熟悉的套路;校验,双层循环;那我们从key值比较相等(代码中 /////////////////////////////////////////////// 相等 处)开始说起

  1. 如果删除数据的value值不为空且不相等,表明不存在想删除数据,跳出双层循环,结束
  2. 当前数据节点是想删除的数据节点,数据value原子操作置空,失败,内循环重新开始
  3. 删除数据value = null,原子操作去除数据节点并进行后驱处理,失败,则通过findNode来去除节点(其通过自旋+cas操作一定能操作成功)
  4. 删除数据节点,并处理后驱成功;则通过 findPredecessor方法进行无效索引节点去除,同样此操作肯定可以成功;
  5. 如果最高层head索引节点,右指针域为空,尝试去除最高层索引层(已经没有任何数据域索引节点了)
  6. 返回删除节点的value值

2.10 释放索引层 tryReduceLevel

    private void tryReduceLevel() {
        HeadIndex<K,V> h = head;
        HeadIndex<K,V> d;
        HeadIndex<K,V> e;
        if (h.level > 3 &&
            (d = (HeadIndex<K,V>)h.down) != null &&
            (e = (HeadIndex<K,V>)d.down) != null &&
            e.right == null &&
            d.right == null &&
            h.right == null &&
            casHead(h, d) && 
            h.right != null)
            casHead(d, h); 
    }

去除索引条件

  1. 至少有4层索引节点
  2. head节点的down指针域不为空
  3. head节点的down指针域的down指针域不为空
  4. head的right指针域为空
  5. head的down指针域的右域为空
  6. head节点的down指针域的down指针域的右域为空
  7. 原子操作 head 置换为head的down指针域,失败,结束
  8. 当前head的右指针域不为空,则原子操作head,重新回退为h

为啥要这么多判断啊,为啥要回退啊,我是完全不懂,只能说肯定是为了处理线程同步问题的,哪位读者知道,谢谢留言告诉我

3、总结

  1. 存储包括数据单链表、索引单链表结构;索引单链表有两个指针域,右、下
  2. 数据单链表,是从小到大排列的
  3. 索引数据域,每个数据对应的索引层数不是有序分布的
  4. 增加数据时,随机数决定是否增加索引层,是否增加插入值索引;这个随机很重要,影响着效率
  5. 删除数据后,可能减少索引层
  6. head索引以及其down、down-down指针域的节点值是固定的
  7. 未加入数据时有一层索引,一个固定头数据
  8. 查找时,通过索引查找最大左区间数据节点,然后再数据链表中向后查询
  9. 插入数据的key类,必须实现Comparator接口或者初始化时传入比较器

技术变化都很快,但基础技术、理论知识永远都是那些;作者希望在余后的生活中,对常用技术点进行基础知识分享;如果你觉得文章写的不错,请给与关注和点赞;如果文章存在错误,也请多多指教!