简单说说ConcurrentSkipListMap

1,557 阅读17分钟

基本介绍

跳跃表的性质如下:

  • 最底层的数据节点按照关键字key升序排列
  • 包含多级索引,每个级别的索引节点按照其关联的数据节点的关键字key升序排列
  • 高级别索引是其低级别索引的子集。
  • 如果关键字key在级别level=i的索引中出现,则级别level<=i的所有索引都包含该key

跳跃表ConcurrentSkipListMap的数据结构如下图所示,下图一共有三层索引,最底下为数据节点,同一层索引中,索引节点之间使用right指针相连,上层索引节点的down指针指向下层的索引节点。 未命名文件 (1).png

源码分析

核心字段分析

  • head 指向 node(BASE_HEADER) 的顶层索引。
/**
 * The topmost head index of the skiplist.
 */
private transient volatile HeadIndex<K,V> head;
  • BASE_HEADER 头结点,即最底层(level=1)索引的头节点的value值
/**
 * Special value used to identify base-level header
 */
private static final Object BASE_HEADER = new Object()
  • Node 静态内部类,即数据节点
/**
 * 数据节点
 */
static final class Node<K,V> {
    final K key;//数据节点的key
    volatile Object value;//数据节点的value
    volatile Node<K,V> next;//指向下一个数据节点

    /**
     * Creates a new regular node.
     */
    Node(K key, Object value, Node<K,V> next) {
        this.key = key;
        this.value = value;
        this.next = next;
    }
}
  • Index 静态内部类,即普通索引节点
/**
 * 普通索引节点
 */
static class Index<K,V> {
    final Node<K,V> node;//索引节点指向的数据节点
    final Index<K,V> down;//当前索引节点的正下方索引节点
    volatile Index<K,V> right;//当前索引节点的右索引节点

    /**
     * Creates index node with given values.
     */
    Index(Node<K,V> node, Index<K,V> down, Index<K,V> right) {
        this.node = node;
        this.down = down;
        this.right = right;
    }
}
  • HeadIndex 静态内部类,即当前级别索引的头节点
/**
 * 当前级别索引的头节点
 */
static final class HeadIndex<K,V> extends Index<K,V> {
    final int level;//所处索引级别
    /**
     * node:当前索引指向的数据节点
     * down:当前索引节点的正下方索引节点
     * right:当前索引节点的右索引节点
     * level:当前索引头节点所处的索引级别
     */
    HeadIndex(Node<K,V> node, Index<K,V> down, Index<K,V> right, int level) {
        super(node, down, right);
        this.level = level;
    }
}

查询

根据指定的key查询节点,源码如下:

public V get(Object key) {
    //调用doGet方法
    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)                // 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) {
                @SuppressWarnings("unchecked") V vv = (V)v;
                return vv;
            }
            if (c < 0)
                break outer;
            b = n;
            n = f;
        }
    }
    return null;
}

在上述代码中,outer处的for自旋中,首先查看findPredecessor查询指定key节点的前驱节点。该方法在下面的好多地方会调用,例如插入元素,删除元素以及删除元素对应的索引时都会调用。

findPredecessor方法源码如下:

/**
 * 作用1:找到key对应节点的前驱节点,不一定的真的前驱节点,也可能是前驱结点的前驱节点
 * 作用2:删除无效的索引,即要删除节点时,将节点的索引也删除掉
 */
private Node<K,V> findPredecessor(Object key, Comparator<? super K> cmp) {
    if (key == null)
        throw new NullPointerException(); // don't postpone errors
    for (;;) {
        //r为q节点的右指针指向的节点,r为当前比较节点,每次都比较r节点的key跟查找的key的大小关系
        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;           // restart
                    r = q.right;         // reread r
                    continue;
                }
                //当前查找的key比r节点的key大,所以r、q节点都向右移动
                if (cpr(cmp, key, k) > 0) {
                    q = r;
                    r = r.right;
                    continue;
                }
            }
            //当q的下方索引节点为空,则说明已经到数据节点层了,需要退出进行后续查找处理
            if ((d = q.down) == null)
                return q.node;
            /**
             * 此时当前查找的key小于r节点的key,需要往下一级索引查找
             * d节点赋值为为q节点为正下方节点,即下一级索引的正下方节点
             */
            q = d;
            r = d.right;
        }
    }
}

findPredecessor方法的查找过程图示如下:假设要查找节点6

跳跃表查询步骤1.png

由于当前r节点的key比查询的key小,所以,r、q节点都向右移动,即执行如下代码:

//当前查找的key比r节点的key大,所以r、q节点都向右移动
if (cpr(cmp, key, k) > 0) {
    q = r;
    r = r.right;
    continue;
}

跳跃表查询步骤2.png

此时r节点指向的数据节点为10,10节点的key比6节点的key大,此时需要执行如下代码:

/**
 * 此时当前查找的key小于r节点的key,需要往下一级索引查找
 * d节点赋值为为q节点为正下方节点,即下一级索引的正下方节点
 */
q = d;
r = d.right;

跳跃表查询步骤3.png

此时r节点指向的数据节点为5,5节点的key比6节点的key小,q、r节点向右移动,如下图所示

跳跃表查询步骤4.png

此时r节点指向的数据节点为10,10节点的key比6节点的key大,同理需要往下级索引走,如下图所示:

跳跃表查询步骤5.png

此时r节点指向的数据节点为10,10节点的key比6节点的key大,同理需要往下级索引走,但是此时下一级索引为空了,即(d = q.down) == null了,此时执行的代码如下, 返回q索引指向的节点,即返回节点5.

//当q的下方索引节点为空,则说明已经到数据节点层了,需要退出进行后续查找处理
if ((d = q.down) == null)
    return q.node;

以上就是方法findPredecessor的查找流程,咱们接着继续看上面的doGet方法

private V doGet(Object key) {
    if (key == null)
        throw new NullPointerException();
    Comparator<? super K> cmp = comparator;
    outer: for (;;) {
        //n节点为b节点的下一个节点
        for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
            Object v; int c;
            if (n == null)
                break outer;
            //f节点为n节点的下一个节点
            Node<K,V> f = n.next;
            if (n != b.next)                // inconsistent read
                break;
            //如果发现n节点的value为null,说明n节点已经欸删除了
            if ((v = n.value) == null) {    // n is deleted
                //当前线程帮助删除对应节点
                n.helpDelete(b, f);
                break;
            }
            if (b.value == null || v == n)  // b is deleted
                break;
            //如果n节点key与查找的key一样的话,说明n就是要查找的节点,返回n节点的value值
            if ((c = cpr(cmp, key, n.key)) == 0) {
                @SuppressWarnings("unchecked") V vv = (V)v;
                return vv;
            }
            if (c < 0)
                break outer;
            //如果n节点的key小于要查询的key,则b,n,f节点均向下移动一个节点
            b = n;
            n = f;
        }
    }
    return null;
}

首先初始化b、n、f三个节点,如下图所示

跳跃表查询步骤6.png 发现此时n节点指向的节点就是要查询的节点,于是执行如下代码:

if ((c = cpr(cmp, key, n.key)) == 0) {
    //说明n就是要查找的节点,返回n节点的value值
    @SuppressWarnings("unchecked") V vv = (V)v;
    return vv;
}

直接返回n节点的value值。查询操作完成。

插入

跳跃表的插入操作分以下四种情况:

  • 情况1:跳跃表内存在key一致元素,做替换

  • 情况2:插入新元素,无须给新元素生成索引节点

  • 情况3:插入新元素,需要给新元素生成索引节点,且索引高度 < maxLevel

  • 情况4:插入新元素,需要给新元素生成索引节点,且索引高度 > maxLevel

源码如下:

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) {
    //z变量最终指向k-v归属的Node节点
    Node<K,V> z;             // added node
    if (key == null)
        throw new NullPointerException();
    Comparator<? super K> cmp = comparator;
    //outer循环,处理并发冲突,进行重试...等其他需要重试的情况
    outer: for (;;) {
        for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
            //下面为让当前插入的key与节点n作比较
            if (n != null) {
                Object v; int c;
                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) {
                    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
                }
                // else c < 0; fall through
            }

            z = new Node<K,V>(key, value, n);
            //通过cas方式插入节点z
            if (!b.casNext(n, z))
                break;         // restart if lost race to append to b
            break outer;
        }
    }

    /**
     * 下面的代码为判断是否需要给新插入的节点创建索引
     */
    int rnd = ThreadLocalRandom.nextSecondarySeed();
    /**
     * 0x80000001 => 1000 0000 0000 0000 0000 0000 0000 0001,即最高位和最后一位为1,其余全部是0
     * 条件:(rnd & 0x80000001) == 0什么时候成立?rnd这个随机数最低位和最高位同时是0的时候,条件成立,概率是1/4
     */
    if ((rnd & 0x80000001) == 0) { // test highest and lowest bits
        /**
         * level:表示z(新插入)节点的索引级别
         * max:表示当前跳跃表索引最大级别
         */
        int level = 1, max;
        /**
         * 计算出当前z节点到底应该有多少级别索引,算法:rnd从第二位开始,向后延伸,看有多少个1相连
         * 举个例子:
         * rnd = 0000 0000 0000 0000 0000 0000 0000 0110 计算出来的level => 3
         * rnd = 0000 0000 0000 0000 0000 0000 1111 1110 计算出来的level => 8
         */
        while (((rnd >>>= 1) & 1) != 0)
            ++level;
        // 计算得到的 level => 3

        //idx:最终指向z节点未处理前后关系的索引,即索引还没有加入到当前层级的索引
        Index<K,V> idx = null;
        //h:指向head,跳跃表的左上角的index
        HeadIndex<K,V> h = head;

        // 假设 得到的数是:0b 0000 1111 0000 1111 0000 1111 0000 0110,计算得出的 level = 3
        // 假设 h.level = 3 ,该条件成立...
        if (level <= (max = h.level)) {
            for (int i = 1; i <= level; ++i)
                idx = new Index<K,V>(z, idx, null);
            // index-3   ← idx
            //   ↓
            // index-2
            //   ↓
            // index-1
            //   ↓
            // z-node
        }
        // 假设 得到的数是:0b 0000 1111 0000 1111 0000 1111 0000 1110,计算得出的z节点索引 level = 4
        // 假设 h.level = 3,会执行到else,z节点的计算出来的索引level大于当前跳跃表的最大索引级别3
        else { // try to grow by one level
            // level 值被重置了,重置为  原 maxLevel + 1,比如说 原head->index.level = 3, =》 level = 4
            level = max + 1; // hold in array and later pick the one to use

            // 创建一个index数组,长度是 level + 1,假设 level is 4, 创建的数组长度为 5.
            @SuppressWarnings("unchecked")Index<K,V>[] idxs =
                    (Index<K,V>[])new Index<?,?>[level+1];

            // 看完这个for循环 就清楚了...原来 index[0] 的这个数组slot 并没有 使用...只使用 [1,level] 这些数组slot了。
            for (int i = 1; i <= level; ++i)
                // index-4   ← idx
                //   ↓
                // index-3
                //   ↓
                // index-2
                //   ↓
                // index-1
                //   ↓
                //  z-node
                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;
                // newh 最终会指向 最新的 headIndex,马上会看到要给baseHeader提升索引
                HeadIndex<K,V> newh = h;
                // oldbase 指向 baseHeader 节点。
                Node<K,V> oldbase = h.node;

                // 升级 baseHeader 索引,升高一级。,即for只会执行一次
                // 让升高的索引的右指针指向上面新插入节点z的最高索引
                for (int j = oldLevel+1; j <= level; ++j)
                    newh = new HeadIndex<K,V>(oldbase, newh, idxs[j], j);
                // 执行完for循环之后,baseHeader 索引长这个样子..让升高的索引的右指针指向上面新插入节点z的最高索引
                // index-4             →             index-4
                //   ↓                                  ↓
                // index-3                           index-3  ← idx
                //   ↓                                  ↓
                // index-2                           index-2
                //   ↓                                  ↓
                // index-1                           index-1
                //   ↓                                  ↓
                // baseHeader         ....            z-node


                // cas成功后,map.head 字段指向 最新的 headIndex,即上图中 baseHeader 的 index-4 节点。
                if (casHead(h, newh)) {
                    // h 指向最新的 index-4 节点, idx 指向 z-node 的index-3 节点。
                    // 为什么 idx 要指向 z-node 的 index-3 节点?
                    // 因为从index-3 ~ index-1 的这些z-node的索引节点 都没有插入到 索引链表...接下来要做的事情就是 将 这些 索引节点 插入链表。
                    h = newh;
                    idx = idxs[level = oldLevel];
                    break;
                }
            }
        }

        // idx 指向z-node 最上层的 且 尚未与前驱 index 串联起来的 index。
        // 情况1:z-node 计算出来的z-level <= headLevel,假设 headLevel = 3, z-level = 3
        // => idx -> index-3
        // 情况2:z-node 计算出来的z-level > headLevel,假设 headLevel = 3, z-level = 5,
        // 程序接下来会重置z-level = headLevel + 1 => 4,并创建出 高度为 4 的index 索引。
        // => idx -> index-3


        // insertionLevel 代表 z-node 尚未处理 队列关系的 层级...
        // 比如 情况1 时,insertionLevel = 3 z-node索引没有超过之前的最大索引级别
        //     情况2 时,insertionLevel = 3 z-node索引超过之前的最大索引级别
        // find insertion points and splice in
        splice: for (int insertionLevel = level;;) {
            // baseHeader 最高层的index level 赋值给j。 情况1 时 是 3,情况2 时  是 4
            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 recheck
                    int c = cpr(cmp, key, n.key);
                    if (n.value == null) {
                        if (!q.unlink(r))
                            break;
                        r = q.right;
                        continue;
                    }
                    //比较r节点的key与新插入节点z的key大小,如果小于的话,则r,q都向右移
                    if (c > 0) {
                        q = r;
                        r = r.right;
                        continue;
                    }
                }

                // 执行到这里,说明r r节点的key比新插入节点z的key要大,即c < 0
                if (j == insertionLevel) {
                    //连接新插入节点z的索引
                    if (!q.link(r, t))
                        break; // restart
                    if (t.node.value == null) {
                        findNode(key);
                        break splice;
                    }
                    if (--insertionLevel == 0)
                        break splice;
                }

                if (--j >= insertionLevel && j < level)
                    //继续连接新插入节点z-node的下一级索引,t指向z-node的下一级索引
                    t = t.down;
                //q指向整个跳跃表的索引下一级,继续处理
                q = q.down;
                r = q.right;
            }
        }
    }
    return null;
}

首先还是跟查询操作类似,调用findPredecessor方法先查找到待插入key的前驱节点,举个例子,例如我们想要插入节点7,如下图所示:

跳跃表插入步骤1.png

接着跟查询操作一样的步骤如下,直接看图:

跳跃表插入步骤2.png 此时r节点指向数据节点1,节点1的key小于待插入的节点7的key,于是节点q、r同时向右移动。

跳跃表插入步骤3.png

此时r节点指向数据节点10,节点10的key大于待插入节点7的key,于是往下一层索引继续查找,执行的代码如下:

d = q.down;
q = d;
r = d.right;

跳跃表插入步骤4.png

后面的操作类似

跳跃表插入步骤5.png

跳跃表插入步骤6.png

此时r节点的key大于待插入的节点6的key,但是q节点的down指针已为空,此时直接返回q节点指向的节点5。

跳跃表插入步骤7.png

接着回到doPut方法,先来查看outer循环,如下:

outer: for (;;) {
    for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
        //下面为让当前插入的key与节点n作比较
        if (n != null) {
            Object v; int c;
            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;
            //如果n节点的key小于待插入的key,则b,n,f节点均向下移动一个节点    
            if ((c = cpr(cmp, key, n.key)) > 0) {
                b = n;
                n = f;
                continue;
            }
            //如果待插入的Key与n节点的key的话,直接将数据通过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节点,z节点就是待插入的节点
        z = new Node<K,V>(key, value, n);
        //通过cas方式插入节点z
        if (!b.casNext(n, z))
            break;         // restart if lost race to append to b
        break outer;
    }
}

首先初始化三个节点b、n、fn节点为b节点的下一个节点,而f节点为n节点的下一个节点,如下图所示

跳跃表插入步骤8.png

接着比较节点n与待插入的key的大小,此时n节点的key小于待插入节点的key,于是b、n、f三个节点均向下移动如下图所示

跳跃表插入步骤9.png

此时n节点的key大于待插入的key,此时执行如下代码,通过cas方式修改b节点的下一个节点为z节点,接着跳出outer循环。

//此时`n`节点的`key`大于待插入的`key`
//创建出z节点,就是待插入的节点
z = new Node<K,V>(key, value, n);
//通过cas方式插入节点z
if (!b.casNext(n, z))
	break;         // restart if lost race to append to b

/**
 * 修改n节点的而下一个节点为z节点
 */
boolean casNext(Node<K,V> cmp, Node<K,V> val) {
    return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}

然后我们知道doPut剩下的代码无非就是判断是否给新插入的节点z创建索引,如果需要创建对应的索引。

首先通过int rnd = ThreadLocalRandom.nextSecondarySeed();计算出一个随机数,接着进行如下判断:

if ((rnd & 0x80000001) == 0) {
    //给新插入的节点创建索引
}

如果rnd & 0x80000001) == 0就给新插入的z节点创建索引,我们知道0x80000001 = 1 000 0000 0000 0000 0000 0000 0000 0001最高位和最后一位为1,其余全部是0

条件:(rnd & 0x80000001) == 0什么时候成立?

rnd这个随机数最低位和最高位同时是0的时候,条件成立,概率是1/4

举个例子:例如rnd = 0000 0000 0000 0000 0000 0000 0000 0110 = 3条件就成立。

如果条件成立的话,接着计算到底给z节点创建几级索引,代码如下:

/**
 * level:表示z(新插入)节点的索引级别
 * max:表示当前跳跃表索引最大级别
 */
int level = 1, max;
/**
 * 计算出当前z节点到底应该有多少级别索引,算法:rnd从第二位开始,向后延伸,看有多少个1相连
 * 举个例子:
 * rnd = 0000 0000 0000 0000 0000 0000 0000 0110 计算出来的level => 3
 * rnd = 0000 0000 0000 0000 0000 0000 1111 1110 计算出来的level => 8
 */
while (((rnd >>>= 1) & 1) != 0)
    ++level;
// 计算得到的 level => 3

通过while条件((rnd >>>= 1) & 1) != 0满足几次就创建几级索引。例如:

  • rnd = 0000 0000 0000 0000 0000 0000 0000 0110 计算出来的level => 3
  • rnd = 0000 0000 0000 0000 0000 0000 1111 1110 计算出来的level => 8

然后接着比较计算出来的z节点的索引跟现有的跳跃表的索引级别大小。

  • 情况一:z节点计算出来的索引level比跳跃表的level小
  • 情况二:z节点计算处理的索引level比跳跃表的level大。此时会选择最终的level为原来的调表的level + 1

情况一

z节点创建索引的步骤如下图所示,此时z节点的索引还没有加入跳跃表现有的索引队列中

跳跃表插入步骤10.png

接着继续执行splice循环,代码如下:

splice: for (int insertionLevel = level;;) {
    // baseHeader 最高层的index level 赋值给j。 情况1 时 是 3,情况2 时  是 4
    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 recheck
            int c = cpr(cmp, key, n.key);
            if (n.value == null) {
                if (!q.unlink(r))
                    break;
                r = q.right;
                continue;
            }
            //比较r节点的key与新插入节点z的key大小,如果小于的话,则r,q都向右移
            if (c > 0) {
                q = r;
                r = r.right;
                continue;
            }
        }

        // 执行到这里,说明r r节点的key比新插入节点z的key要大,即c < 0
        if (j == insertionLevel) {
            //连接新插入节点z的索引
            if (!q.link(r, t))
                break; // restart
            if (t.node.value == null) {
                findNode(key);
                break splice;
            }
            if (--insertionLevel == 0)
                break splice;
        }

        if (--j >= insertionLevel && j < level)
            //继续连接新插入节点z-node的下一级索引,t指向z-node的下一级索引
            t = t.down;
        //q指向整个跳跃表的索引下一级,继续处理
        q = q.down;
        r = q.right;
    }
}

初始化q、r节点如下图所示

跳跃表插入步骤12.png

此时r节点的key比新插入z节点,即7节点小,于是两个节点q、t都向右移动如下图所示

跳跃表插入步骤13.png

此时r节点的key比新插入z节点,即7节点大,执行如下代码:

//将q节点,t节点为idx指向的节点,r节点连接起来
q.link(r, t)
//继续连接新插入节点z-node的下一级索引,t指向z-node的下一级索引
t = t.down;
//q指向整个跳跃表的索引下一级,继续处理
q = q.down;
r = q.right;

跳跃表插入步骤14.png

此时r节点的key比新插入z节点,即7节点小,于是两个节点q、t都向右移动如下图所示

跳跃表插入步骤15.png

此时r节点的key比新插入z节点,即7节点大,同理,直接看图

跳跃表插入步骤16.png

跳跃表插入步骤17.png

情况二

跟情况一类似,这里就不一一画图了

删除

删除方法完成的任务如下:

    1. 设置指定元素value为null
    1. 将指定node从node链表移除
    1. 将指定node的index节点 从 对应的 index 链表移除
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 (;;) {
        //找到key对应节点的前驱节点,不一定的真的前驱节点,也可能是前驱结点的前驱节点
        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;
            }

            //找到要删除的key节点了
            if (value != null && !value.equals(v))
                break outer;
            //将节点的值设置为null
            if (!n.casValue(v, null))
                break;
            /**
             * n.appendMarker(f):在n节点跟f之间插入一个mark节点,添加mark节点成功,返回true,取反得到false
             * b.casNext(n, f):将b节点的next指针指向f节点,这一步成功,n节点就正式出队了,返回true,取反得到false
             */
            if (!n.appendMarker(f) || !b.casNext(n, f))
                findNode(key);                  // retry via findNode
            else {
                //findPredecessor清楚无效的索引,即上面删除的节点的索引
                findPredecessor(key, cmp);      // clean index
                if (head.right == null)
                    tryReduceLevel();
            }
            @SuppressWarnings("unchecked") V vv = (V)v;
            return vv;
        }
    }
    return null;
}

同样,首先通过findPredecessor方法查找到要删除key的前驱节点,就不一一画图了,直接看找到的前驱节点的图,如下:

跳跃表删除步骤1.png

接比较n节点的key与待删除的key的大小,此时n节点的key小于待删除的key,即7节点的key,于是将b、n、f三个节点都向右移动,如下图:

跳跃表删除步骤2 .png

此时n节点的key跟待删除的key一样,于是执行如下代码:

//将节点的值设置为null
if (!n.casValue(v, null))
    break;

/**
 * n.appendMarker(f):在n节点跟f之间插入一个mark节点,添加mark节点成功,返回true,取反得到false
 * b.casNext(n, f):将b节点的next指针指向f节点,这一步成功,n节点就正式出队了,返回true,取反得到false
 */
if (!n.appendMarker(f) || !b.casNext(n, f))
    findNode(key);                  // retry via findNode
else {
    //findPredecessor清楚无效的索引,即上面删除的节点的索引
    findPredecessor(key, cmp);      // clean index
    if (head.right == null)
        tryReduceLevel();
}

/**
 * 创建一个mark节点
 */
boolean appendMarker(Node<K,V> f) {
    return casNext(f, new Node<K,V>(f));
}

跳跃表删除步骤3.png

最后再调用findPredecessor清楚无效的索引,即上面删除的节点的索引。

private Node<K,V> findPredecessor(Object key, Comparator<? super K> cmp) {
    if (key == null)
        throw new NullPointerException(); // don't postpone errors
    for (;;) {
        //r为q节点的右指针指向的节点,r为当前比较节点
        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;           // restart
                    r = q.right;         // reread r
                    continue;
                }
                if (cpr(cmp, key, k) > 0) {
                    q = r;
                    r = r.right;
                    continue;
                }
            }
            //d节点赋值为为q节点为正下方节点,即下一级索引的正下方节点
            if ((d = q.down) == null)
                return q.node;
            q = d;
            r = d.right;
        }
    }
}

重点靠如下代码块删除索引的:

//该节点已经删除,需要删除其对应的索引
if (n.value == null) {
    //该节点已经删除,需要删除其对应的索引
    if (!q.unlink(r))
        break;           // restart
    r = q.right;         // reread r
    continue;
}


final boolean unlink(Index<K,V> succ) {
    return node.value != null && casRight(succ, succ.right);
}

我们知道在上面已经将待删除的7节点的value置为null了,直接看图:

跳跃表删除步骤4.png

此时r节点的key小于待删除节点的key,于是r、q节点都向右移动。

跳跃表删除步骤5.png

此时r,n节点指向的数据节点的value值为null于是执行上面的q.unlink(r)代码,将q的右指针指向r的右指针指向的节点,即就是删除了该level上的7节点的索引节点,如下图所示

跳跃表删除步骤6.png

此时r节点的key大于待删除节点的key,于是往下一索引走,如下图所示

跳跃表删除步骤7.png

此时r节点的key小于待删除节点的key,于是r、q节点都向右移动。

跳跃表删除步骤8.png

此时r,n节点指向的数据节点的value值为null于是执行上面的q.unlink(r)代码,将q的右指针指向r的右指针指向的节点,即就是删除了该level上的7节点的索引节点,如下图所示

跳跃表删除步骤9.png

后续操作同理,最终将7节点的索引一一删除完,最终的图下所示

跳跃表删除步骤10.png