zoukankan      html  css  js  c++  java
  • JUC之ConcurrentSkipListMap源码分析

    一、ConcurrentSkipListMap简介

      ConcurrentSkipListMap是线程安全的有序的哈希表,适用于高并发的场景

      ConcurrentSkipListMap和TreeMap,它们虽然都是有序的哈希表。但是

     1. 它们的线程安全机制不同,TreeMap是非线程安全的,而ConcurrentSkipListMap是线程安全的。

     2. ConcurrentSkipListMap是通过跳表实现的,而TreeMap是通过红黑树实现的

      ConcurrentHashMap存储速度比ConcurrentSkipListMap快。在4线程1.6万数据的条件下,CHM是CSLM的4倍

      但是ConcurrentSkipListMap有几个不能比拟的优点:

    1. ConcurrentSkipListMap的key是有序的。

    2. ConcurrentSkipListMap支持更高的并发。ConcurrentSkipListMap 的存取时间是log(N),和线程数几乎无关。也就是说在数据量一定的情况下,并发的线程越多,ConcurrentSkipListMap越能体现出他的优势。

    类的继承结构

    ConcurrentSkipListMap的类继承图:

      一般的Map都是无限的,只能通过键的hash值定位。JDK为了实现有序的Map,提供了SortedMap接口,SortedMap提供了一些根据键范围进行查找的功能,比如返回Map中的key的Max/Min的键等等

      为了进一步对有序Map进行增强,JDK又引入了NavigableMap接口,该接口进一步扩展了SortedMap的功能,提供了根据指定Key返回最接近项、按升序/降序返回所有键的视图等功能。

      同时,也提供了一个基于NavigableMap的实现类——TreeMap,TreeMap底层基于红黑树设计,是一种有序的Map。

    ConcurrentSkipListMap的由来

      JDK1.6时,为了对高并发环境下的有序Map提供更好的支持,J.U.C新增了一个ConcurrentNavigableMap接口,ConcurrentNavigableMap很简单,它同时实现了NavigableMap和ConcurrentMap接口:

              

      ConcurrentNavigableMap接口提供的功能也和NavigableMap几乎完全一致,很多方法仅仅是返回的类型不同。

      J.U.C提供了基于ConcurrentNavigableMap接口的一个实现——ConcurrentSkipListMap

      ConcurrentSkipListMap可以看成是并发版本的TreeMap,但是和TreeMap不同是,ConcurrentSkipListMap并不是基于红黑树实现的,其底层是一种类似跳表(Skip List)的结构

    二、Skip List简介

    什么是Skip List

      Skip List(简称跳表),一种类似链表的数据结构,其查询/插入/删除的时间复杂度都是O(logn)。

      我们知道,通常意义上的链表是不能支持随机访问的(通过索引快速定位),其查找的时间复杂度是O(n),而数组是可支持随机访问的数据结构,虽然查找快,但是插入/删除元素却需要移动插入点后的所有元素,时间复杂度O(n)

      为了解决这一问题,引入了树结构,树的增删改查效率比平均,一颗平衡二叉树(AVL)的增删改查效率一般为O(logn),比如工业上常用红黑树作为AVL的一种实现。

      但是,AVL的实现一般都很复杂,插入/删除元素可能涉及对整个树结构的修改,特别是并发环境下,通常需要全局锁来保证AVL的线程安全,于是又出现了一种类似链表的数据结构——跳表

    Skip List示例

      在讲Skip List之前,我们先来看下传统的单链表:

      

      上图的单链表中(省去了结点之间的链接),当想查找7、15、46这三个元素时,必须从头指针head开始,遍历整个单链表,其查找复杂度很低,为O(n)

      来看下Skip List的数据结构是什么样的:

      

      上图是Skip List一种可能的结构,它分了2层,假设我们要查找“15”这个元素,那么整个步骤如下:

    1. 从头指针head开始,找到第一个结点的最上层,发现其指向的下个结点值为8,小于15,则直接从1结点跳到8结点。
    2. 8结点最上层指向的下一结点值为18,大于15,则从8结点的下一层开始查找。
    3. 从8结点的最下层一直向后查找,依次经过10、13,最后找到15结点。

      上述整个查找路径如下图标黄部分所示:

      

      同理,如果要查找“46”这个元素,则整个查找路径如下图标黄部分所示:

      

      上面就是跳跃表的基本思想了,每个结点不仅仅只包含指向下一个结点的指针,可能还包含很多个其它指向后续结点的指针。并且,一个结点本身可以看成是一个链表(自上向下链接)。这样就可以跳过一些不必要的结点,从而加快查找、删除等操作,这其实是一种“空间换时间”的算法设计思想。

      那么一个结点可以包含多少层呢 比如,Skip List也可能是下面这种包含3层的结构(在一个3层Skip List中查找元素“46”):

      

      层数是根据一种随机算法得到的,为了不让层数过大,还会有一个最大层数MAX_LEVEL限制,随机算法生成的层数不得大于该值。后面讲ConcurrentSkipListMap时,我们会具体分析。

      以上就是Skip List的基本思想了,总结起来,有以下几点:

    1. 跳表由很多层组成;
    2. 每一层都是一个有序链表
    3. 对于每一层的任意结点,不仅有指向下一个结点的指针,也有指向其下一层的指针

    三、ConcurrentSkipListMap的内部结构

    ConcurrentSkipListMap的数据结构,如下图所示:

       

    介绍完了跳表,再来看ConcurrentSkipListMap的内部结构就容易得多了: 

       

      ConcurrentSkipListMap内部一共定义了3种不同类型的节点,元素的增删改查都是从最上层的head指针指向的节点开始。

    节点定义

    普通节点:Node

      普通节点——Node,也就是ConcurrentSkipListMap最底层链表中的节点,保存着实际的键值对,如果单独看底层链,其实就是一个按照Key有序排列的单链表:

     static final class Node<K,V> {
            final K key;
            volatile Object value;
            volatile Node<K,V> next;
            // 新建常规节点
            Node(K key, Object value, Node<K,V> next) {
                this.key = key;
                this.value = value;
                this.next = next;
            }
            // 标记节点
            Node(Node<K,V> next) {
                this.key = null;
                this.value = this;
                this.next = next;
            }
            // CAS更新节点的value
            boolean casValue(Object cmp, Object val) {
                return UNSAFE.compareAndSwapObject(this, valueOffset, cmp, val);
            }
            // CAS更新节点的next
            boolean casNext(Node<K,V> cmp, Node<K,V> val) {
                return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
            }
            // 判断当前节点是否为标记节点
            boolean isMarker() {
                return value == this;
            }
            // 判断当前节点是否为最底层链表的头结点
            boolean isBaseHeader() {
                return value == BASE_HEADER;
            }
            // 在当前节点后插入一个标记节点, f为当前节点的后继节点
            boolean appendMarker(Node<K,V> f) {
                return casNext(f, new Node<K,V>(f));
            }
            // 辅助删除节点方法。 b为当前节点的前驱节点;f为当前节点的后继节点
            void helpDelete(Node<K,V> b, Node<K,V> f) {
                // 重新检查一遍节点的位置,确保 b 和 f 分别为当前节点的前驱/后继。
                if (f == next && this == b.next) {
                    if (f == null || f.value != f) // f为null 或 非标记节点
                        casNext(f, new Node<K,V>(f));
                    else                 //删除当前节点
                        b.casNext(this, f.next);
                }
            }
            // 返回节点的value值
            V getValidValue() {
                Object v = value;
                if (v == this || v == BASE_HEADER) // 如果是标记节点或最底层头结点,返回null
                    return null;
                @SuppressWarnings("unchecked") 
            V vv = (V)v; return vv; } // 返回当前节点的一个SimpleImmutableEntry快照 AbstractMap.SimpleImmutableEntry<K,V> createSnapshot() { Object v = value; if (v == null || v == this || v == BASE_HEADER) return null; @SuppressWarnings("unchecked") V vv = (V)v; return new AbstractMap.SimpleImmutableEntry<K,V>(key, vv); } private static final sun.misc.Unsafe UNSAFE; private static final long valueOffset; private static final long nextOffset; static { try { UNSAFE = sun.misc.Unsafe.getUnsafe(); Class<?> k = Node.class; valueOffset = UNSAFE.objectFieldOffset (k.getDeclaredField("value")); nextOffset = UNSAFE.objectFieldOffset (k.getDeclaredField("next")); } catch (Exception e) { throw new Error(e); } } }

    索引节点

      Index节点是除底层链外,其中各层链表中的非头结点。每个Index节点包含3个指针:downrightnode

      down和right指针分别指向下层节点和后继节点,node指针指向其最底部的node节点。

    static class Index<K,V> {
            final Node<K,V> node; //node指向最底层链表的Node节点
            final Index<K,V> down; //down指向下层Index节点
            volatile Index<K,V> right;  //right指向右边的Index节点
            // 创建索引节点
            Index(Node<K,V> node, Index<K,V> down, Index<K,V> right) {
                this.node = node;
                this.down = down;
                this.right = right;
            }
            // CAS更新右边的Index节点, cmp当前节点的右节点,val希望更新的节点
            final boolean casRight(Index<K,V> cmp, Index<K,V> val) {
                return UNSAFE.compareAndSwapObject(this, rightOffset, cmp, val);
            }
            // 判断Node节点是否已删除
            final boolean indexesDeletedNode() {
                return node.value == null;
            }
            // CAS插入一个右边节点newSucc
            final boolean link(Index<K,V> succ, Index<K,V> newSucc) {
                Node<K,V> n = node;
                newSucc.right = succ;
                return n.value != null && casRight(succ, newSucc);
            }
            // 跳过当前节点的后继节点
            final boolean unlink(Index<K,V> succ) {
                return node.value != null && casRight(succ, succ.right);
            }
            // Unsafe mechanics
            private static final sun.misc.Unsafe UNSAFE;
            private static final long rightOffset;
            static {
                try {
                    UNSAFE = sun.misc.Unsafe.getUnsafe();
                    Class<?> k = Index.class;
                    rightOffset = UNSAFE.objectFieldOffset
                        (k.getDeclaredField("right"));
                } catch (Exception e) {
                    throw new Error(e);
                }
            }
        }

    头索引节点HeadIndex

    HeadIndex节点是各层链表的头结点,它是Index类的子类,唯一的区别是增加一个level字段,用于表示当前链表的级别,也就是属于哪一层,越往上层,level值越大。

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

    构造器

    ConcurrentSkipListMap一共定义了4种构造器:

    // 构造一个新的空Map.
    public ConcurrentSkipListMap() {
        this.comparator = null;
        initialize();
    }
    // 指定比较器的构造器
    public ConcurrentSkipListMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
        initialize();
    }
    //从给定Map构建的构造器
    public ConcurrentSkipListMap(Map<? extends K, ? extends V> m) {
        this.comparator = null;
        initialize();
        putAll(m);
    }
    //从给定SortedMap构建的构造器
    //从已给定的SortedMap构造一个新Map,并且Key的顺序与原来保持一致.
    public ConcurrentSkipListMap(SortedMap<K, ? extends V> m) {
        this.comparator = m.comparator();
        initialize();
        buildFromSorted(m);
    }

    注:ConcurrentSkipListMap会基于比较器——Comparator,来进行键Key比较,如果构造时未指定Comparator,那么就会按照Key的自然顺序进行比较,所谓Key的自然顺序是指key实现Comparable接口。

    上述所有构造器都调用了initialize方法:

    private void initialize() {
        keySet = null;
        entrySet = null;
        values = null;
        descendingMap = null;
        head = new HeadIndex<K, V>(new Node<K, V>(null, BASE_HEADER, null),null, null, 1);
    }

    initialize方法将一些字段置初始化null,然后将head指针指向新创建的HeadIndex节点。初始化完成后,ConcurrentSkipListMap的结构如下:

      

    其中,headBASE_HEADER都是ConcurrentSkipListMap的字段

    // 最底层链表的头指针BASE_HEADER
    private static final Object BASE_HEADER = new Object()
    // 最上层链表的头指针head
    private transient volatile HeadIndex<K, V> head;

    四、ConcurrentSkipListMap的核心操作

    put操作

      put操作本身很简单,需要注意的是ConcurrentSkipListMap在插入键值对时,Key和Value都不能为null:

    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;             // added node
            if (key == null)  // key不可以为空
                throw new NullPointerException();
            Comparator<? super K> cmp = comparator;  //比较器的引用
            outer: for (;;) {
            // 根据key,找到待插入的位置。
            // b 叫前驱节点,作为新加入节点的前驱节点;n 叫后继节点,作为新加入节点的后继节点。也就是说,新节点将插入b和n之间
    for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
              
    //如果n为null,那么说明b是链表的最尾端的节点,这种情况比较简单,直接构建新节点插入即可。否则走下面判断
    if (n != null) { Object v; int c; Node<K,V> f = n.next;
                 // 如果n不再是b的后继节点,说明有线程指向b后面添加了新元素,那么我们直接退出内循环,重新计算新节点要插入的位置
    if (n != b.next) // inconsistent read break;
                 // value = 0 说明n已经被标识位待删除,其他线程正在进行删除操作
                 // 调用helpDelete帮助删除,并退出内层循环重新结算待插入位置
    if ((v = n.value) == null) { // n is deleted n.helpDelete(b, f); break; }
                 // b 已经被标记为待删除,前驱节点b 都丢了,得重新计算待插入位置
    if (b.value == null || v == n) // b is deleted break;
                 // 如果新节点的key大于n 的key 说明找到的前驱节点有误,按序往后挪一个位置即可    
                 // 回到内层循环重新试图插入
    if ((c = cpr(cmp, key, n.key)) > 0) { b = n; n = f; continue; }
                 // 新节点的key等于n的key,这是一次update操作,CAS更新即可
                 // 如果更新失败,重新进循环再来一次
    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 } // else c < 0; fall through }           // 无论遇到何种问题,到这一步说明待插位置已经确定
    z = new Node<K,V>(key, value, n); if (!b.casNext(n, z)) break; // restart if lost race to append to
              //如果成功了,退出最外层循环,完成了底层的插入工
    break outer; } } // 以上这一部分主要完成了向底层链表插入一个节点,至于其中具体的怎么找前驱节点的方法稍后介绍。但这其实只不过才完成一小半的工作,就像红黑树在插入后需要 rebalance 一样,
    // 我们的跳表需要根据概率算法保证节点分布稳定,它的调节措施相对于红黑树来说就简单多了,通过往上层索引层添加相关引用即可,以空间换时间。      // 获取一个线程无关的随机数,占四个字节,32个比特位
    int rnd = ThreadLocalRandom.nextSecondarySeed();
         // 如果等于0, 说明这个随机数最高位和最低位都为0,需要增加层级,这种概率很大;如果不等于0,那么仅仅将新节点插入到最底层的链表中即可,不会往上层递归
    if ((rnd & 0x80000001) == 0) {int level = 1, max; while (((rnd >>>= 1) & 1) != 0) // level表示新的层级,通过下面这个while循环可以确认新的层级数 ++level; Index<K,V> idx = null; HeadIndex<K,V> h = head;
            //如果概率算得的 层数 level 在当前跳表已有的层数 level 范围内       //构建一个从 1 到 level 的纵列 index 结点引用
    if (level <= (max = h.level)) { // CASE1: 新层级level没有超过最大层级head.level(head指针指向最高层) for (int i = 1; i <= level; ++i) idx = new Index<K,V>(z, idx, null); }
            // 如果计算得到的层数 大于 当前跳表的最大层数
            // 需要新增一个 level 层
    else {                 // CASE2: 新层级level超过了最大层级head.level level = max + 1;         // 重置level为最大层级+1 @SuppressWarnings("unchecked")
              Index<K,V>[] idxs = (Index<K,V>[])new Index<?,?>[level+1]; //生成一个Index结点数组,idxs[0]不会使用 for (int i = 1; i <= level; ++i) idxs[i] = idx = new Index<K,V>(z, idx, null);
              // 生成新的HeadIndex节点
    for (;;) { h = head; int oldLevel = h.level;
                 // level 肯定是比 oldLevel 大1的,如果小了说明其他线程更新过表了
    if (level <= oldLevel) break; HeadIndex<K,V> newh = h; Node<K,V> oldbase = h.node;
                 //正常情况下,循环只会执行一次,如果由于其他线程的并发操作导致 oldLevel 的值不稳定,那么会执行多次循环体
    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; } } }
    // 这一部分的代码主要完成的是根据 level 的值,确认是否需要增加一层索引,如果不需要则构建好底层到 level 层的 index 结点的纵向引用。
    // 如果需要,则新创建一层索引,完成 head 结点的指针转移,并构建好纵向的 index 结点引用。
    // 以下方法用于链接新层级的各个HeadIndex和Index结点 splice: for (int insertionLevel = level;;) { // 此时level为oldLevel,即原最大层级 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; // compare before deletion check avoids needing recheckint c = cpr(cmp, key, n.key);
                   //如果 n 正在被其他线程删除,那么调用 unlink 去删除它
    if (n.value == null) { if (!q.unlink(r)) break;
                     //重新获取 q 的右结点,再次进入循环 r
    = q.right; continue; }
                   // c > 0 说明前驱结点定位有误,重新进入
    if (c > 0) { q = r; r = r.right; continue; } } if (j == insertionLevel) {
                   //尝试着将 t 插在 q 和 r 之间,如果失败了,退出内循环重试
    if (!q.link(r, t)) break; // restart
                   //如果插入完成后,t 结点被删除了,那么结束插入操作if (t.node.value == null) { findNode(key); break splice; }
                   // insertionLevel-- 处理底层链接
    if (--insertionLevel == 0) break splice; }             //--j,j 应该与 insertionLevel 同步,它代表着我们创建的那个纵向的结点数组的索引   //并完成层次下移操作 if (--j >= insertionLevel && j < level) t = t.down;
                //至此,新节点在当前层次的前后引用关系已经被链接完成,现在处理下一层 q
    = q.down; r = q.right; } } } returnnull
    ; }
    // 我们根据概率算法得到了一个 level 值,并且通过第二步创建了 level 个新节点并构成了一个纵向的引用关联,但是这些纵向的结点并没有链接到每层中。
    // 而我们的第三部分代码就是完成的这个工作,将我们的新节点在每个索引层都构建好前后的链接关系。下面用三张图描述着三个部分所完成的主要工作。

    新增节点图解

    初始的跳表如下:

     第一部分,新增一个结点到最底层的链表上。

     第二部分,假设概率得出一个 level 值为 10,那么根据跳表的算法描述需要新建一层索引层。

    第三步,链接各个索引层次上的新节点。

    这样就完成了新增结点到跳表中的全部过程,大体上已如上图描述,至于 ConcurrentSkipListMap 中关于并发处理的细节之处,图中无法展示,大家可据此重新感受下源码的实现过程

    我们来看看doPut方法中的findPredecessor方法,根据给定的key,为我们返回最合适的前驱节点

    // 返回key的最合适的前驱结点. 如果不存在这样的数据结点,则返回底层链表的头结点. @param key 待查找的键
    private Node<K, V> findPredecessor(Object key, Comparator<? super K> cmp) {
        if (key == null)
            throw new NullPointerException();
    
        // 从最上层开始,往右下方向查找
        for (; ; ) {
            for (Index<K, V> q = head, r = q.right, d; ; ) {    // 从最顶层的head结点开始查找
                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) { // key大于k,继续向右查找
                        q = r;
                        r = r.right;
                        continue;
                    }
                }
    
                //已经到了level1的层
                if ((d = q.down) == null)   // 不存在下结点,说明q已经是level1链表中的结点了
                    return q.node;          // 直接返回对应的Node结点
    
           // 转到下一层,继续查找(level-1层)
                q = d;
                r = d.right;
            }
        }
    }

    示意图

      

       上图中,假设要查找的Key为72,则步骤如下:

    1. 从最上方head指向的结点开始,比较①号标红的Index结点的key值,发现3小于72,则继续向右;
    2. 比较②号标红的Index结点的key值,发现62小于72,则继续向右
    3. 由于此时右边是null,则转而向下,一直到⑥号标红结点;
    4. 由于⑥号标红结点的down字段为空(不能再往下了,已经是level1最低层了),则直接返回它的node字段指向的结点,即⑧号结点

    remove操作

    public V remove(Object key) {
        return doRemove(key, null);
    }
    final V doRemove(Object key, Object value) {
            if (key == null)  // key不可为空
                throw new NullPointerException();
            Comparator<? super K> cmp = comparator; //比较器引用
            outer: for (;;) {
           //
    找到 key 的前驱节点,因为删除不单单是根据 key 找到对应的结点,然后赋 null 就完事的,还要负责链接该结点前后的关联
    for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) { Object v; int c; if (n == null) // n 就是要删除的节点,如果为空,已经删除。就不用做什么了 break outer; Node<K,V> f = n.next; //删除节点的后继节点
              
    //再次确认 n 还是不是 b 的后继结点,如果不是将退出里层循环重新进入
    if (n != b.next) break;
              // 如果有人正在删除n,那么帮助它删除
    if ((v = n.value) == null) { // n is deleted n.helpDelete(b, f); break; }
              // b 被删除了,重新定位前驱节点
    if (b.value == null || v == n) // b is deleted break;
              
    //正常情况下,key 应该等于 n.key。key 大于 n.key 说明我们要找的结点可能在 n 的后面,往后递归即可;key 小于 n.key 说明 key 所代表的结点根本不存在
    if ((c = cpr(cmp, key, n.key)) < 0) break outer; if (c > 0) { b = n; n = f; continue; }
              
    //如果删除是根据键和值两个参数来删除的话,value 是不为 null 的这种情况下,如果 n 的 value 属性不等于我们传入的 value ,那么是不进行删除的
    if (value != null && !value.equals(v)) break outer;
                   //下面三个步骤才是整个删除操作的核心,大致的逻辑我们也在上文提及过了,此处想必会容易理解些    //第一步,尝试将待删结点的 value 属性赋值 null,失败将退出重试if (!n.casValue(v, null)) break;
              //第二步和第三步如果有一步由于竞争失败,将调用 findNode 方法根据我们第一步的成果,也就是删除所有 value 为 null 的结点
    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; }

    remove 方法其实从整体上来看。

      首先会有一堆的判断,根据给定的 key 和 value 会判断是否存在与 key 对应的一个节点,也会判断和待删结点相关的前后结点是否正在被删除,并适情况帮助删除。

      其次才是删除的三大步骤,核心步骤还是将待删结点的 value 属性赋 null 以标记该结点无用了,至于这个 marker 也是为了分散并发冲突的,最后通过 casNext 完成结点的删除。

    图例

    ① 假设现在要删除Key==23的结点,删除前ConcurrentSkipListMap的结构如下:

    doRemove方法首先会找到待删除的结点,在它和后继结点之间插入一个value为null的标记结点(如下图中的绿色结点),然后改变其前驱结点的指向:

    最后,doRemove会重新调用一遍findPredecessor方法,解除被删除结点上的Index结点之间的引用:

    这样Key==23的结点其实就被孤立,再后续查找或插入过程中,会被完全清除或被GC回收。


    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;          // b -> n -> f
                if (n != b.next)
                    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) {  // c = 0 说明n就是我们要的节点
                    V vv = (V) v;
                    return vv;
                }
                if (c < 0)  // c < 0 说明不存在这个key所对应的节点
                    break outer;
                b = n;
                n = f;
            }
        }
        return null;
    }

    参考: https://blog.csdn.net/qq_35326718/article/details/78870658

      https://segmentfault.com/a/1190000016168566

  • 相关阅读:
    linux查看内存占用情况
    tomcat JVM内存 配置
    Linux中iptables设置详细
    asmack xmpp 获取离线消息
    linux查看端口被哪个服务占用的命令
    Redis 3.0版本启动时出现警告的解决办法
    redis中密码设置
    linux安装(Ubuntu)——(二)
    Linux简介——(一)
    Android控件——ToggleButton多状态按钮(实现灯泡的开关)
  • 原文地址:https://www.cnblogs.com/FondWang/p/12144269.html
Copyright © 2011-2022 走看看