ConcurrentSkipListMap-análisis de código fuente de lista de omisión

ConcurrentSkipListMap-análisis de código fuente de lista de omisión

pregunta

  • ¿Cómo es un reloj de salto?
  • ¿Cómo encuentra la tabla de saltos los datos clave especificados?
  • ¿Cómo agregar datos de clave-valor especificados a la tabla de omisión ?
  • ¿ Cómo elimina la tabla de salto los datos clave especificados?

conocimientos teóricos

La lista de salto es una estructura de datos aleatoria, esencialmente una lista enlazada ordenada que puede realizar una búsqueda binaria .

La tabla de salto agrega un índice de varios niveles a la lista enlazada ordenada original y realiza una búsqueda rápida a través del índice.

Las tablas de salto no solo pueden mejorar el rendimiento de la búsqueda, sino también el rendimiento de las operaciones de inserción y eliminación.

  • La estructura de datos de la tabla de salto SkipList

imagen-20221224202012685

Los nodos de nivel inferior, cada uno de los cuales es un nodo Nodo, se clasifican en orden ascendente por clave. Los nodos con niveles superiores son nodos de índice. La tabla de salto en la figura anterior es una tabla de salto con 3 capas de índices. La densidad de índice de la primera capa de nivel 1 es la más alta, y la segunda capa de nivel 2 es el índice. del índice de la primera capa El nivel 3 de la tercera capa es el mismo, la densidad se vuelve más pequeña y el tramo es más grande a medida que se sube.

head es un campo muy importante en ConcurrentSkipListMap. Asocia toda la lista de saltos a través de head. Siempre apunta al nodo de índice headIndex en la parte superior de BaseHeader, y usa este nodo de índice como una entrada para acceder a toda la lista de saltos.

  • Principios de las tablas de salto
    • Los nodos de datos inferiores se ordenan en orden ascendente por clave
    • Contiene un índice de varios niveles, y los nodos de índice de cada nivel se organizan en orden ascendente de acuerdo con la clave clave de su nodo de datos asociado
    • Los índices de alto nivel son un subconjunto de los índices de bajo nivel
    • Si la palabra clave clave level = iaparece en el índice de nivel, entonces level <= iel índice de nivel contiene clave

Análisis de código fuente

Campos y Estructuras de Datos Internos

  • Nodo principal BASE_HEADER
  • head apunta al índice superior del nodo (BASE_HEADER)
  • Clase interna estática de nodo (el nodo inferior almacena clave-valor)
  • Índice de clase interna estática (nodo de índice)
  • Clase interna estática HeadIndex (nodo de índice principal)
public class ConcurrentSkipListMap<K,V> extends AbstractMap<K,V> 
	implements ConcurrentNavigableMap<K,V>, Cloneable, Serializble {
    
    
    // 最底层的节点的head
    private static final Object BASE_HEADER = new Object();
    // head 节点
    private transient volatile HeadIndex<K,V> head;
    // 比较器
    final Comparator<? super K> comparator;
    // key
    private transient KeySet<K> keySet;
    // 元组
    private transient EntrySet<K,V> entrySet;
    // 值
    private transient Values<V> values;
    private transient ConcurrentNavigableMap<K,V> descendingMap;
    
    // cas 设置head
    private boolean casHead(HeadIndex<K,V> cmp, HeadIndex<K,V> val) {
    
    
    	return UNSAFE.compareAndSwapObject(this, headOffset, cmp, val);
    }
    
    /* ---------------- Nodes -------------- */
    
    // 最底层node节点,单链表结构
    static final class Node<K,V> {
    
    
    	final K key;
        // 注意:这里value的类型是Object,而不是V
    	// 在删除元素的时候value会指向当前元素本身
    	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;
        }
        
        boolean casNext(Node<K,V> cmp, Node<K,V> val) {
    
    
            return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
        }
        
        // 协助删除
        void helpDelete(Node<K,V> b, Node<K,V> f) {
    
    
            if (f == next && this == b.next) {
    
    
                if (f == null || f.value != f) // not already marked
                    casNext(f, new Node<K,V>(f));
                else
                    b.casNext(this, f.next);
            }
        }
    }

    /* ---------------- Indexing -------------- */
    
    // 索引节点,存储着对应的node值,及向下和向右的索引指针
    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;
    	}
        /**
         * compareAndSet right field
         */
        final boolean casRight(Index<K,V> cmp, Index<K,V> val) {
    
    
            return UNSAFE.compareAndSwapObject(this, rightOffset, cmp, val);
        }

        /**
         * Returns true if the node this indexes has been deleted.
         * @return true if indexed node is known to be deleted
         */
        final boolean indexesDeletedNode() {
    
    
            return node.value == null;
        }
        
        // 链接节点
        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);
        }
    }
    
    /* ---------------- Head nodes -------------- */
    
 	/**
 	 * Nodes heading each level keep track of their level.
 	 */
    // 头索引节点,继承自Index,并扩展一个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;
         }
 	}

     /**
      * 节点比较逻辑
      * c:比较器
      */
	static final int cpr(Comparator c, Object x, Object y) {
    
    
 		return (c != null) ? c.compare(x, y) : ((Comparable)x).compareTo(y);
 	}
}    

Método de construcción

    public ConcurrentSkipListMap() {
    
    
        this.comparator = null;
        initialize();
    }

    public ConcurrentSkipListMap(Comparator<? super K> comparator) {
    
    
        this.comparator = comparator;
        initialize();
    }

    public ConcurrentSkipListMap(Map<? extends K, ? extends V> m) {
    
    
        this.comparator = null;
        initialize();
        putAll(m);
    }

    public ConcurrentSkipListMap(SortedMap<K, ? extends V> m) {
    
    
        this.comparator = m.comparator();
        initialize();
        buildFromSorted(m);
    }

El método initialize() se llama en los cuatro métodos de construcción, entonces, ¿qué hay en este método?

    private static final Object BASE_HEADER = new Object();

    private void initialize() {
    
    
        keySet = null;
        entrySet = null;
        values = null;
        descendingMap = null;
        // Node(K key, Object value, Node<K,V> next)
        // HeadIndex(Node<K,V> node, Index<K,V> down, Index<K,V> right, int level)
        head = new HeadIndex<K,V>(new Node<K,V>(null, BASE_HEADER, null),
                                  null, null, 1);
    }

Se puede ver que inicializa algunas propiedades y crea un nodo de índice principal, que almacena un nodo de datos.El valor de este nodo de datos es un objeto vacío, y su nivel es 1.

Por lo tanto, en el momento de la inicialización, solo hay un nodo de índice principal en la tabla de saltos, el nivel es 1, el nodo de datos es un objeto vacío y tanto abajo como a la derecha son nulos.

imagen-20221224212046658

A través de la estructura de la clase interna, sabemos que un puntero de índice principal contiene tres punteros de nodo, abajo y derecha, para mayor comprensión, representamos el puntero que apunta al nodo con una línea punteada y los otros dos con un sólido. línea, es decir, la línea punteada no indica la dirección de.

agregar elemento poner () => doPut ()

  • Agregar el valor-clave especificado a la tabla de salto
    public V put(K key, V value) {
    
    
        // 不能存储value为null的元素
        // 因为value为null标记该元素被删除(后面会看到)
        if (value == null)
            throw new NullPointerException();
        
        // 调用doPut()方法添加元素
        return doPut(key, value, false);
    }
				||
                \/

    private V doPut(K key, V value, boolean onlyIfAbsent) {
    
    
        // 添加元素后存储在z中
        Node<K,V> z;             // added node
        // key也不能为null
        if (key == null)
            throw new NullPointerException();
        Comparator<? super K> cmp = comparator;

        /*
         * Part I:找到目标节点的位置并插入
         */            
        // 这里的目标节点是数据节点,也就是最底层的那条链
        // 自旋 outer循环 处理并发冲突 进行重试..等其它需要重试的情况
        outer: for (;;) {
    
    
            // 寻找目标节点之前最近的一个索引对应的数据节点,存储在b中,b=before
            // 并把b的下一个数据节点存储在n中,n=next
            // 为了便于描述,我这里把b叫做当前节点,n叫做下一个节点
            for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
    
    
                // 如果下一个节点不为空
                // 就拿其key与目标节点的key比较,找到目标节点应该插入的位置
                if (n != null) {
    
    
                    // v=value,存储节点value值
                    // c=compare,存储两个节点比较的大小
                    Object v; int c;
                    // n的下一个数据节点,也就是b的下一个节点的下一个节点(孙子节点)
                    Node<K,V> f = n.next;
                    // 如果n不为b的下一个节点
                    // 说明有其它线程修改了数据,则跳出内层循环
                    // 也就是回到了外层循环自旋的位置,从头来过
                    if (n != b.next)               // inconsistent read
                        break;
                    // 如果n的value值为空,说明该节点已删除,协助删除节点
                    if ((v = n.value) == null) {
    
       // n is deleted
                        // todo 这里为啥会协助删除?后面讲
                        n.helpDelete(b, f);
                        break;
                    }
                    // 如果b的值为空或者v等于n,说明b已被删除
                    // 这时候n就是marker节点,那b就是被删除的那个
                    if (b.value == null || v == n) // b is deleted
                        break;
                    // 如果目标key与下一个节点的key大
                    // 说明目标元素所在的位置还在下一个节点的后面
                    if ((c = cpr(cmp, key, n.key)) > 0) {
    
    
                        // 就把当前节点往后移一位
                        // 同样的下一个节点也往后移一位
                        // 再重新检查新n是否为空,它与目标key的关系
                        b = n;
                        n = f;
                        continue;
                    }
                    // 如果比较时发现下一个节点的key与目标key相同
                    // 说明链表中本身就存在目标节点
                    if (c == 0) {
    
    
                        // 则用新值替换旧值,并返回旧值(onlyIfAbsent=false)
                        if (onlyIfAbsent || n.casValue(v, value)) {
    
    
                            @SuppressWarnings("unchecked") V vv = (V)v;
                            return vv;
                        }
                        // 如果替换旧值时失败,说明其它线程先一步修改了值,从头来过
                        break; // restart if lost race to replace value
                    }
                    // 如果c<0,就往下走,也就是找到了目标节点的位置
                    // else c < 0; fall through
                }
				
                /*
                 * 有两种情况会到这里
                 * 1.到链表尾部了,也就是n为null了
                 * 2.找到了目标节点的位置,也就是上面的c<0
                 */

                // 新建目标节点,并赋值给z
                // 这里把n作为新节点的next
                // 如果到链表尾部了,n为null,这毫无疑问
                // 如果c<0,则n的key比目标key大,相当于在b和n之间插入目标节点z
                z = new Node<K,V>(key, value, n);
                // 原子更新b的下一个节点为目标节点z
                if (!b.casNext(n, z))
                    // 如果更新失败,说明其它线程先一步修改了值,从头来过
                    break;         // restart if lost race to append to b
                // 如果更新成功,跳出自旋状态
                break outer;
            }
        }

        // 经过Part I,目标节点已经插入到有序链表中了
        
        /*
         * Part II:随机决定是否需要建立索引及其层次,如果需要则建立自上而下的索引
         */ 

        // 取个随机数
        int rnd = ThreadLocalRandom.nextSecondarySeed();
        // 0x80000001展开为二进制为10000000000000000000000000000001
        // 只有两头是1
        // 这里(rnd & 0x80000001) == 0
        // 相当于排除了负数(负数最高位是1),排除了奇数(奇数最低位是1)
        // 只有最高位最低位都不为1的数跟0x80000001做&操作才会为0(也就是最高位和最低位都为0)
        // 也就是正偶数,概率为1/4
        if ((rnd & 0x80000001) == 0) {
    
     // test highest and lowest bits
            // level 表示z(新插)节点的索引级别,默认level为1,也就是只要到这里了就会至少建立一层索引
            // max 表示当前跳跃表 索引最大级别
            int level = 1, max;
            // 随机数从最低位的第二位开始,有几个连续的1则level就加几
            // 因为最低位肯定是0,正偶数嘛
            // 比如,1100110,从最低位的第二位往前数,总共有两个1连续,level就加2
            while (((rnd >>>= 1) & 1) != 0)
                ++level;

            // idx 最终指向z节点未处理前后关系的索引(用于记录目标节点建立的最高的那层索引节点)
            Index<K,V> idx = null;
            // 取头索引节点(这是最高层的头索引节点,左上角的HeadIndex)
            HeadIndex<K,V> h = head;
            // case1
            // 如果生成的层数小于等于当前最高层的层级
            // 也就是跳表的高度不会超过现有高度
            if (level <= (max = h.level)) {
    
    
                // 从第一层开始建立一条竖直的索引链表
                // 这条链表使用down指针连接起来
                // 每个索引节点里面都存储着目标节点这个数据节点
                // 最后idx存储的是这条索引链表的最高层节点
                for (int i = 1; i <= level; ++i)
                    idx = new Index<K,V>(z, idx, null);
                    // index-3  ← idx
                    //    ↓(down指针)
                    // index-2
                    //    ↓(down指针)
                    // index-1
                    //    ↓(down指针)
                    //  Node(z)
            }
            // case2
            else {
    
     // try to grow by one level
                // 重新计算level的值,设置成更合理的值
                // 如果新的层数超过了现有跳表的高度,则最多只增加一层
                // 比如现在只有一层索引,那下一次最多增加到两层索引,增加多了也没有意义
                level = max + 1; // hold in array and later pick the one to use
                // idxs用于存储目标节点建立的竖起索引的所有索引节点
                // 其实这里直接使用idx这个最高节点也是可以完成的
                // 只是用一个数组存储所有节点要方便一些
                // 注意,这里数组0号位是没有使用的
                @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)
                    idxs[i] = idx = new Index<K,V>(z, idx, null);
                	// index-4  ← idx
                 	//    ↓(down指针)
                    // index-3
                    //    ↓(down指针)
                    // index-2
                    //    ↓(down指针)
                    // index-1
                    //    ↓(down指针)
                    //  Node(z)
                
                // 自旋
                for (;;) {
    
    
                    // 旧的最高层头索引节点
                    h = head;
                    // 旧的最高层级
                    int oldLevel = h.level;
                    // 再次检查,如果旧的最高层级已经不比新层级矮了
                    // 说明有其它线程先一步修改了值,从头来过
                    // 一般不成立..只有在并发情况下才有可能成立
                    if (level <= oldLevel) // lost race to add level
                        break;
                    // 新的最高层头索引节点(newh最终指向新的headIndex,要给baseHeader提升索引),目前指向旧的最高层头索引节点
                    HeadIndex<K,V> newh = h;
                    // 头节点指向的数据节点
                    Node<K,V> oldbase = h.node;
                    // 超出的部分建立新的头索引节点
                    for (int j = oldLevel+1; j <= level; ++j)
                        // 正常这个语句只会执行1次
                        //                       oldbase:BaseHeader 
                        //                       newh:down指针↓(原来的最高头索引节点)
                        //                       idxs[j]:right指针→(新的最高层索引节点)
                        //                       j:level,新level
                        newh = new HeadIndex<K,V>(oldbase, newh, idxs[j], j);
                    	// 执行完for循环之后,baseHeader 索引长这个样子..
                        // headIndex-4             →             index-4    ← idx
                        //   	↓                                   ↓
                        // headIndex-3                           index-3
                        //   	↓                                   ↓
                        // headIndex-2                           index-2
                        //   	↓                                   ↓
                        // headIndex-1                           index-1
                        //   	↓                                   ↓ 
                        // baseHeader         	....             Node(z)
                    	// 但是headIndex-3、2、1还没有跟右边的索引节点串起来
                    // 原子更新头索引节点
                    // cas成功后,skipListMap.head 字段指向 最新的 headIndex,即上图中 baseHeader 的左上角 index-4 节点。
                    if (casHead(h, newh)) {
    
    
                        // h指向新的最高层头索引节点
                        h = newh;
                        // 把level赋值为旧的最高层级的
                        // idx指向的不是新的最高的索引节点了
                        // 而是与旧最高层平齐的索引节点
                        // 因为要开始往下处理底层的索引节点了(它们还没串联起来)
                        idx = idxs[level = oldLevel];
                        break;
                    }
                }
            }

            // 经过上面的步骤,有两种情况
            // 1.没有超出高度,新建一条目标节点的索引节点链
            // 2.超出了高度,新建一条目标节点的索引节点链,同时最高层头索引节点同样往上长

            /*
         	 * Part III:将新建的索引节点(包含头索引节点)与其它索引节点通过右指针连接在一起
         	 */

            // 这时level是等于旧的最高层级的,自旋
            // insertionLevel 代表 z-node 尚未处理 队列关系的 层级..
            splice: for (int insertionLevel = level;;) {
    
    
                // h为最高头索引节点
                int j = h.level;

                // 从头索引节点开始遍历
                // 为了方便,这里叫q为当前节点,r为右节点,d为下节点,t为目标节点相应层级的索引
                for (Index<K,V> q = h, r = q.right, t = idx;;) {
    
    
                    // 如果遍历到了最右边,或者最下边,
                    // 也就是遍历到头了,则退出外层循环
                    if (q == null || t == null)
                        break splice;
                    // 如果右节点不为空
                    if (r != null) {
    
    
                        // n是右节点的数据节点,为了方便,这里直接叫右节点的值
                        Node<K,V> n = r.node;
                        // 比较目标key与右节点的值
                        int c = cpr(cmp, key, n.key);
                        // 如果右节点的值为空了,则表示此节点已删除
                        if (n.value == null) {
    
    
                            // 则把右节点删除
                            if (!q.unlink(r))
                                // 如果删除失败,说明有其它线程先一步修改了,从头来过
                                break;
                            // 删除成功后重新取右节点
                            r = q.right;
                            continue;
                        }
                        // 如果比较c>0,表示目标节点还要往右
                        if (c > 0) {
    
    
                            // 则把当前节点和右节点分别右移
                            q = r;
                            r = r.right;
                            continue;
                        }
                    }

                    // 到这里说明已经到当前层级的最右边了
                    // 如果新生成的索引节点高度大于原最高层索引,这里实际是会先走第二个if

                    // 第一个if
                    // j与insertionLevel相等了
                    // (如果此时j=4,insertionLevel=3)实际是先走的第二个if,j自减后应该与insertionLevel相等
                    if (j == insertionLevel) {
    
    
                        // 这里是真正连右指针的地方
                        if (!q.link(r, t))
                            // 连接失败,从头来过
                            break; // restart
                        // t节点的值为空,可能是其它线程删除了这个元素
                        if (t.node.value == null) {
    
    
                            // 这里会去协助删除元素
                            findNode(key);
                            break splice;
                        }
                        // 当前层级右指针连接完毕,向下移一层继续连接
                        // 如果移到了最下面一层,则说明都连接完成了,退出外层循环
                        if (--insertionLevel == 0)
                            break splice;
                    }

                    // 第二个if
                    // j先自减1,再与两个level比较
                    // j、insertionLevel 和 t(idx),三者是对应的,都是还未把右指针连好的那个层级
                    if (--j >= insertionLevel && j < level)
                        // t往下移
                        t = t.down;

                    // 当前层级到最右边了
                    // 那只能往下一层级去走了
                    // 当前节点下移
                    // 再取相应的右节点
                    q = q.down;
                    r = q.right;
                }
            }
        }
        return null;
    }

    // Node.class中的方法,协助删除元素
    void helpDelete(Node<K,V> b, Node<K,V> f) {
    
    
        /*
         * Rechecking links and then doing only one of the
         * help-out stages per call tends to minimize CAS
         * interference among helping threads.
         */
        // 这里的调用者this==n,三者关系是b->n->f
        if (f == next && this == b.next) {
    
    
            // 将n的值设置为null后,会先把n的下个节点设置为marker节点
            // 这个marker节点的值是它自己
            // 这里如果不是它自己说明marker失败了,重新marker
            if (f == null || f.value != f) // not already marked
                casNext(f, new Node<K,V>(f));
            else
                // marker过了,就把b的下个节点指向marker的下个节点
                b.casNext(this, f.next);
        }
    }

    // Index.class中的方法,删除succ节点
    final boolean unlink(Index<K,V> succ) {
    
    
        // 原子更新当前节点指向下一个节点的下一个节点
        // 也就是删除下一个节点
        return node.value != null && casRight(succ, succ.right);
    }

    // Index.class中的方法,在当前节点与succ之间插入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);
    }

imagen-20221225150123857

Punteros B, N, F, Z, asumiendo que el nodo insertado es 7.

  • Caso 1: hay elementos con la misma clave en la tabla de salto, así que reemplácelos

  • Caso 2: Insertar un nuevo elemento sin generar un nodo de índice para el nuevo elemento

  • Caso 3: para insertar un nuevo elemento, se debe generar un nodo de índice para el nuevo elemento y la altura del índice < maxLevel (caso 1 en el código)

    imagen-20221225180848741

  • Caso 4: para insertar un nuevo elemento, se debe generar un nodo de índice para el nuevo elemento y la altura del índice> maxLevel (caso 2 en el código)

    imagen-20221225180918240

Aquí dividimos todo el proceso de inserción en tres partes:

Parte I: Encuentre la posición del nodo de destino e insértelo

(1) El nodo objetivo aquí es el nodo de datos, que es la cadena inferior;

(2) Encuentre el nodo de datos correspondiente al índice más cercano antes del nodo de destino (los nodos de datos están todos en la lista enlazada inferior);

(3) Recorra hacia atrás desde este nodo de datos hasta encontrar la posición en la que debe insertarse el nodo de destino;

(4) Si hay un elemento en esta posición, actualice su valor (onlyIfAbsent = false);

(5) Si no hay ningún elemento en esta posición, inserte el nodo de destino;

(6) Hasta ahora, el nodo de destino se ha insertado en la lista vinculada de nodos de datos de nivel inferior;

Parte II: decidir aleatoriamente si crear un índice y su jerarquía, y crear un índice de arriba hacia abajo si es necesario

(1) Tome un número aleatorio y calcule (rnd & 0x80000001);

(2) Si no es igual a 0, finalizar el proceso de inserción, es decir, no es necesario crear un índice y volver;

(3) Si es igual a 0, ingrese al proceso de creación de un índice (solo los números positivos y pares serán iguales a 0);

(4) Calcular while (((rnd >>>= 1) & 1) != 0)y determinar el número de niveles, el nivel comienza desde 1;

(5) Si el nivel calculado no es superior al nivel más alto existente, cree directamente una lista de índice vertical (solo hacia abajo tiene un valor) y finalice la Parte II;

(6) Si el nivel calculado es más alto que el nivel más alto existente, el nuevo nivel solo puede ser 1 más que el nivel más alto existente;

(7) También cree una lista de índice vertical (solo abajo tiene valor);

(8) Aumentar el índice de la cabeza a la altura correspondiente, finalizando la Parte II;

(9) Es decir, si el nivel no supera la altura existente, solo se establecerá una cadena de índice, de lo contrario, se aumentará adicionalmente la altura de la cadena de índice de la cabeza (piénselo y dé ejemplos más adelante);

Parte III: conecte el nodo de índice recién creado (incluido el nodo de índice principal) con otros nodos de índice a través del puntero derecho (agregue el puntero derecho)

(1) Comenzando desde el nodo de índice principal del nivel más alto, muévase hacia la derecha para encontrar la posición del nodo de índice de destino;

(2) Si la capa actual tiene un índice de destino, inserte el índice de destino en esta posición y mueva el índice anterior del índice de destino un nivel hacia abajo;

(3) Si no hay un índice de destino en la capa actual, mueva el índice anterior del índice de destino un nivel hacia abajo;

(4) De manera similar, muévase hacia la derecha nuevamente para encontrar la posición del índice objetivo en el nuevo nivel y regrese al paso (2);

(5) Circular a su vez para encontrar las posiciones de todos los índices objetivo jerárquicos e insertarlos en la lista de enlaces de índices horizontales;

En resumen, hay tres pasos en total:

(1) Inserte el nodo de destino en la lista vinculada de nodos de datos;

(2) Crear una lista descendente vertical;

(3) Establecer una lista horizontal enlazada a la derecha;

Agregar ejemplo de elemento

Supongamos que la lista enlazada inicial es así:

imagen-20221225132325728

Supongamos que queremos insertar un elemento 9 ahora.

(1) Encuentre el nodo de datos correspondiente al índice más cercano antes del nodo de destino, y aquí está el nodo de datos 5 encontrado;

(2) Recorriendo hacia atrás desde 5 para encontrar la posición del nodo de destino, que está entre 8 y 12;

(3) Insértese el elemento 9, y termina la Parte I;

imagen-20221225132345944

Luego, calcule su nivel de índice, si es 3, es decir, nivel = 3.

(1) Crear una lista enlazada de índice vertical hacia abajo;

(2) Excediendo la altura existente 2, se debe aumentar la altura de la cadena de índice de la cabeza;

(3) En este punto, termina la Parte II;

imagen-20221225132404459

Finalmente, complete el puntero derecho.

(1) Encuentre la posición del índice objetivo del nivel actual desde la cabeza del tercer nivel hacia la derecha;

(2) Cuando lo encuentre, conecte el índice de destino con el puntero derecho de su índice anterior, donde el anterior resulta ser la cabeza;

(3) Luego, el índice anterior se mueve hacia abajo, aquí está la cabeza hacia abajo;

(4) Vaya a la derecha para encontrar la posición del índice objetivo;

(5) Cuando lo encuentre, conecte el puntero derecho, donde el anterior es el índice de 3;

(6) Entonces el índice de 3 se mueve hacia abajo;

(7) Vaya a la derecha para encontrar la posición del índice objetivo;

(8) Cuando lo encuentre, conecte el puntero derecho, donde el anterior es el índice de 5;

(9) Luego 5 se mueve hacia abajo, hasta el final, la Parte III ha terminado y todo el proceso de inserción ha terminado;

imagen-20221225132438262

encontrarPredecesor()

	// 寻找目标节点之前最近的一个索引对应的数据节点
    private Node<K,V> findPredecessor(Object key, Comparator<? super K> cmp) {
    
    
        // key不能为空
        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;
                    // 如果右节点的value为空
                    // 说明其它线程把这个节点标记为删除了
                    // 则协助删除
                    if (n.value == null) {
    
    
                        if (!q.unlink(r))
                            // 如果删除失败
                            // 说明其它线程先删除了,从头来过
                            break;           // restart
                        // 删除之后重新读取右节点
                        r = q.right;         // reread r
                        continue;
                    }
                    // 如果目标key比右节点还大,继续向右寻找
                    if (cpr(cmp, key, k) > 0) {
    
    
                        // 往右移
                        q = r;
                        // 重新取右节点
                        r = r.right;
                        continue;
                    }
                    // 如果c<0,说明不能再往右了
                }
                // 到这里说明当前层级已经到最右了
                // 两种情况:一是r==null,二是c<0
                // 再从下一级开始找

                // 如果没有下一级了,就返回这个索引对应的数据节点
                if ((d = q.down) == null)
                    return q.node;

                // 往下移
                q = d;
                // 重新取右节点
                r = d.right;
            }
        }
    }

quitar elemento remove() => doRemove()

  • Eliminar el elemento correspondiente a la clave especificada
    public V remove(Object key) {
    
    
        return doRemove(key, null);
    }
				||
                \/
                    
    final V doRemove(Object key, Object value) {
    
    
        // key不为空
        if (key == null)
            throw new NullPointerException();
        Comparator<? super K> cmp = comparator;
        // 自旋
        outer: for (;;) {
    
    
            // 寻找目标节点之前的最近的索引节点对应的数据节点
            // 为了方便,这里叫b为当前节点,n为下一个节点,f为下下个节点
            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;
                // 再次检查
                // 如果n不是b的下一个节点了
                // 说明有其它线程先一步修改了,从头来过
                if (n != b.next)                    // inconsistent read
                    break;
                // 如果下个节点的值为null了
                // 说明有其它线程标记该元素为删除状态了
                if ((v = n.value) == null) {
    
            // n is deleted
                    // 协助删除
                    n.helpDelete(b, f);
                    break;
                }
                // 如果b的值为空或者v等于n,说明b已被删除
                // 这时候n就是marker节点,那b就是被删除的那个
                if (b.value == null || v == n)      // b is deleted
                    break;
                // 如果c<0,说明没找到元素,退出外层循环
                if ((c = cpr(cmp, key, n.key)) < 0)
                    break outer;
                // 如果c>0,说明还没找到,继续向右找
                if (c > 0) {
    
    
                    // 当前节点往后移
                    b = n;
                    // 下一个节点往后移
                    n = f;
                    continue;
                }
                // c=0,说明n就是要找的元素
                // 如果value不为空且不等于找到元素的value,不需要删除,退出外层循环(一般传进来的value都为null 这里不会成立)
                if (value != null && !value.equals(v))
                    break outer;
                // 如果value为空,或者相等
                // 原子标记n的value值为空
                if (!n.casValue(v, null))
                    // 如果删除失败,说明其它线程先一步修改了,从头来过
                    break;

                // P.S.到了这里n的值肯定是设置成null了

                // 关键!!!!
                // 让n的下一个节点指向一个market节点
                // 这个market节点的key为null,value为marker自己,next为n的下个节点f
                // 或者让b的下一个节点指向下下个节点
                // 注意:这里是或者||,因为两个CAS不能保证都成功,只能一个一个去尝试
                // 这里有两层意思:
                // 1.如果标记market成功,再尝试将b的下个节点指向下下个节点,如果第二步失败了,进入条件,如果成功了就不用进入条件了
                // 2.如果标记market失败了,直接进入条件
                if (!n.appendMarker(f) || !b.casNext(n, f))
                    // 通过findNode()重试删除(里面有个helpDelete()方法)
                    findNode(key);                  // retry via findNode
                else {
    
    
                    // 上面两步操作都成功了,才会进入这里,不太好理解,上面两个条件都有非"!"操作
                    // 说明节点已经删除了,通过findPredecessor()方法删除索引节点
                    // findPredecessor()里面有unlink()操作,将删除节点的索引与其它索引断开
                    //(核心就是判断当前node节点的value是否为null,为null则将这个索引出队)
                    findPredecessor(key, cmp);      // clean index
                    // 如果最高层头索引节点没有右节点,则跳表的高度降级
                    if (head.right == null)
                        tryReduceLevel();
                }
                // 返回删除的元素值
                @SuppressWarnings("unchecked") V vv = (V)v;
                return vv;
            }
        }
        return null;
    }
  • Establecer el valor del elemento especificado en nulo
  • Eliminar el nodo especificado de la lista de nodos
  • Eliminar el nodo de índice del nodo especificado de la lista vinculada de índice correspondiente

imagen-20221225204450333

(1) Encuentre el nodo de datos correspondiente al índice más cercano antes del nodo de destino (los nodos de datos están todos en la lista enlazada inferior);

(2) Recorra hacia atrás desde este nodo de datos hasta encontrar la ubicación del nodo de destino;

(3) Si no hay ningún elemento en esta posición, devuelve nulo directamente, lo que indica que no hay ningún elemento que eliminar;

(4) Si hay un elemento en esta posición, primero n.casValue(v, null)establezca su valor en nulo a través de la actualización atómica;

(5) n.appendMarker(f)Marque el elemento actual como el elemento a eliminar agregando un elemento marcador detrás del elemento actual;

(6) Intentando b.casNext(n, f)eliminar elementos;

(7) Si alguno de los dos pasos anteriores falla, continúe findNode(key)intentando n.helpDelete(b, f)eliminar;

(8) Si los dos pasos anteriores son exitosos, elimine el nodo de índice por findPredecessor(key, cmp)el medio ;q.unlink(r)

(9) Si el puntero derecho de la cabeza apunta a nulo, la altura de la tabla de salto se degrada;

Ejemplo de eliminar elemento

Si la tabla de salto inicial es como se muestra en la figura siguiente, queremos eliminar el elemento 9.

imagen-20221225132627998

(1) Encuentre el nodo de datos 9;

(2) Establezca el valor del nodo 9 en nulo;

(3) Agregue un nodo de marcador después de 9, y la marca 9 se ha eliminado;

(4) Que 8 apunte a 12;

(5) Desconecte el nodo de índice de la derecha de su índice anterior;

(6) Rebaja de la altura de la mesa de salto;

imagen-20221225132702822

En cuanto a por qué hay tantos pasos (2) (3) (4), porque si deja que 8 apunte directamente a 12 en subprocesos múltiples, otros subprocesos pueden insertar un elemento 10 entre 9 y 12 primero, y en este momento No bien.

Entonces, aquí hay tres pasos para garantizar la corrección de las operaciones de subprocesos múltiples.

Si el paso (2) falla, vuelva a intentarlo directamente;

Si el paso (3) o (4) falla, porque el paso (2) es exitoso, vuelva a intentar eliminar a través de helpDelete();

De hecho, helpDelete() también sigue reintentando (3) y (4);

Solo cuando estos tres pasos se completan correctamente, el elemento se puede eliminar por completo.

Combina esta pieza con el rojo, el verde y el azul de la figura anterior para entenderlo bien, y debes pensar en lo que sucederá en un entorno concurrente.

encontrar elemento get() => doGet()

  • Consultar el valor correspondiente a la clave especificada
    public V get(Object key) {
    
    
        return doGet(key);
    }
				||
                \/
                    
    private V doGet(Object key) {
    
    
        // key不为空
        if (key == null)
            throw new NullPointerException();
        Comparator<? super K> cmp = comparator;
        // 自旋
        outer: for (;;) {
    
    
            // 寻找目标节点之前最近的索引对应的数据节点
            // 为了方便,这里叫b为当前节点,n为下个节点,f为下下个节点
            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;
                // 如果n的值为空,说明节点已被其它线程标记为删除
                if ((v = n.value) == null) {
    
        // n is deleted
                    // 协助删除,再重试
                    n.helpDelete(b, f);
                    break;
                }
                // 如果b的值为空或者v等于n,说明b已被删除
                // 这时候n就是marker节点,那b就是被删除的那个
                if (b.value == null || v == n)  // b is deleted
                    break;
                // 如果c==0,说明找到了元素,就返回元素值
                if ((c = cpr(cmp, key, n.key)) == 0) {
    
    
                    @SuppressWarnings("unchecked") V vv = (V)v;
                    return vv;
                }
                // 如果c<0,说明没找到元素
                if (c < 0)
                    break outer;
                // 如果c>0,说明还没找到,继续寻找
                // 当前节点往后移
                b = n;
                // 下一个节点往后移
                n = f;
            }
        }
        return null;
    }

(1) Encuentre el nodo de datos correspondiente al índice más cercano antes del nodo de destino (los nodos de datos están todos en la lista enlazada inferior);

(2) Recorra hacia atrás desde este nodo de datos hasta encontrar la ubicación del nodo de destino;

(3) Si no hay ningún elemento en esta posición, devolver nulo directamente, indicando que no se encontró ningún elemento;

(4) Si hay un elemento en esta posición, devolver el valor del elemento;

Ejemplo de encontrar un elemento

Si hay una tabla de saltos como se muestra en la figura siguiente, queremos encontrar el elemento 9, ¿cuál es el camino que recorrió? Puede ser diferente de lo que pareces...

imagen-20221226143245705

(1) Encuentre el nodo de datos correspondiente al índice más cercano antes del nodo de destino, aquí es 5;

(2) Recorriendo hacia atrás desde 5, pasando por 8, hasta 9;

(3) encontrado y devuelto;

La ruta completa se muestra en la siguiente figura:

imagen-20221226143248474

no esta muy jodido?

¿Por qué no venir directamente del índice de 9?

Desde el punto de vista de mi depuración de punto de interrupción real, de hecho sigue el camino en la imagen de arriba.

Supongo que puede deberse a que el método findPredecessor() es compartido por múltiples métodos de inserción, eliminación y búsqueda de elementos. La inserción y eliminación de elementos en una lista enlazada individualmente necesita registrar el elemento anterior, pero la búsqueda no lo necesita. Aquí, para ser compatible con los tres Hace que la codificación sea un poco más fácil, por lo que se usa la misma lógica, sin optimizar los elementos de búsqueda individualmente.

Resumir

  • ConcurrentSkipListMap es 空间换时间una aplicación típica de .
  • ConcurrentSkipListMap es la implementación de la lista de exclusión en Java, pero con algún procesamiento concurrente.

huevos

¿Por qué Redis eligió usar listas de salto en lugar de árboles rojo-negro para implementar colecciones ordenadas?

Primero, analicemos las operaciones admitidas por la colección ordenada de Redis:

  • insertar elemento
  • eliminar elemento
  • encontrar elemento
  • salida ordenada de todos los elementos
  • Encuentra todos los elementos en el rango

Entre ellos, se pueden completar los primeros cuatro árboles rojo-negro, y la complejidad del tiempo es consistente con la lista de saltos.

Sin embargo, para el último elemento, la eficiencia del árbol rojo-negro no es tan alta como la de la tabla de salto.

En la tabla de saltos, para encontrar los elementos del intervalo, solo necesitamos ubicar los dos extremos del intervalo en el nivel más bajo y luego recorrer los elementos en orden, lo cual es muy eficiente.

El árbol rojo-negro solo se puede ubicar después del punto final, y luego necesita buscar el nodo sucesor cada vez desde la primera posición, lo que requiere relativamente mucho tiempo.

Además, la tabla de saltos es fácil y legible de implementar, y el árbol rojo-negro es relativamente difícil de implementar, por lo que Redis elige usar la tabla de saltos para implementar la colección ordenada.




referencia

Supongo que te gusta

Origin blog.csdn.net/weixin_53407527/article/details/128445418
Recomendado
Clasificación