Recently in the Redis design and implementation, in Redis underlying data structure used skip table, taking advantage of this demand, looked at Java based on skip table implementation set.

Jump table

A SkipList is an ordered data structure in which each node maintains multiple Pointers to other nodes for fast access. For the most part, skip lists are as efficient and easier to implement as balanced trees because they are so widely used that only ConcurrentSkipListMap is implemented.See what new ConcurrentSkipListMap() does, starting with the main code.

	public ConcurrentSkipListMap(a) {
		// comparator is a unified comparator
    	this.comparator = null;
    	initialize();
	}
	private void initialize(a) {
        keySet = null;
        entrySet = null;
        values = null;
        descendingMap = null;
        head = new HeadIndex<K,V>(new Node<K,V>(null, BASE_HEADER, null),
                                  null.null.1);
    }
    /** header index */
    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; }}Index / * * * /
    static class Index<K.V> {
        final Node<K,V> node;
        final Index<K,V> down;
        volatile Index<K,V> right;
        
 		Index(Node<K,V> node, Index<K,V> down, Index<K,V> right) {
            this.node = node;
            this.down = down;
            this.right = right;
        }
Copy the code

Here you can see new ConcurrentSkipListMap(), which just creates a raw header index, no hierarchy, no linked list. Now let’s look at his put method. Let’s say we want to put key 5.

    public V put(K key, V value) {
        if (value == null)
            throw new NullPointerException();
        return doPut(key, value, false);
    }
Copy the code

The doPut() method is too long, so we split the method into three parts

1. Create the node to be inserted

First, find the appropriate Node location. Note that the Node obtained here does not necessarily need to be inserted in front of the Node. The obtained Node satisfies two conditions:

1. At the bottom; 2. The key of the node is smaller than that of the newly inserted nodeCopy the code

Then, we set the current traversal node as B, the next node of the current traversal node as N, and the new node to be inserted as Z. Recursively find the insertion point, and the insertion point needs to meet two conditions:

1. b < z <= n 2. b is closest to NCopy the code
private V doPut(K key, V value, boolean onlyIfAbsent) {
        // z is the Node to be created
        Node<K,V> z;             // added node
        if (key == null)
            throw new NullPointerException();
        Comparator<? super K> cmp = comparator;
        outer: for (;;) {
            /* * b: the node traversed currently, which is initially the front node of the newly inserted node * n: is the next node traversed currently, which is initially the rear node of b before the new node is inserted * */
            for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
                if(n ! =null) {
                    Object v; int c;
                    // f: subsequent nodes of n
                    Node<K,V> f = n.next;
                    // If n is not a subsequent node of b, do the next for (;;). , possibly because other threads have inserted other nodes into subsequent nodes of B
                    if(n ! = b.next)// inconsistent read
                        break;
                    // If the value of subsequent node n is null, it indicates that the node has been deleted, then delete the node, replace n with the subsequent node f of n, the next for (;;).
                    if ((v = n.value) == null) {   // n is deleted
                        n.helpDelete(b, f);
                        break;
                    }
                    // If the value of b is null, it is deleted by another thread. For (;;)
                    if (b.value == null || v == n) // b is deleted
                        break;
                    // If the key to be inserted is larger than that of the subsequent node, proceed to the subsequent node
                    if ((c = cpr(cmp, key, n.key)) > 0) {
                        b = n;
                        n = f;
                        continue;
                    }
                    // If the key to be inserted is the same as the key of the subsequent node
                    if (c == 0) {
                        // If onlyIfAbsent is true, replace the value of n to end the loop
                        // If the cas replacement fails, do the next for (;;)
                        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
                }

                // Create node z to be inserted, next pointing to n
                z = new Node<K,V>(key, value, n);
                // Change the next point of b to new node Z, cas replacement failed, next for (;;)
                if(! b.casNext(n, z))break;         // restart if lost race to append to b
                // At this point, the bottom Node list is formed, but for skip lists, the top index and its join relationships need to be constructed
                breakouter; }}Copy the code

There are several major methods, starting with the findPredecessor(Key, CMP) method, which is used to find suitable Node locations

    /** * return a node that is placed at the bottom of the skip table, and only a node that meets the following two conditions: 2. The key of the node is smaller than that of the newly inserted node * It does not mean that the returned node is followed by the newly inserted node, This does not mean that the last nodes of the newly inserted node will be the same as the original q.ext *. The relationship between the last nodes is as follows: q --> XXX --> newly inserted node --> XXX --> original q.ext * XXX indicates that there may be multiple interval * variables. The current index, and the index * r that needs to contain the final result node: the subsequent index of the current index, or whether the key of the subsequent index is greater than the key */ that needs to be inserted
    private Node<K,V> findPredecessor(Object key, Comparator<? super K> cmp) {
        if (key == null)
            throw new NullPointerException(); // don't postpone errors
        for (;;) {
            // Get the original header index q and the subsequent index r of the current index
            for (Index<K,V> q = head, r = q.right, d;;) {
                // if r is null, it means that the index level is not larger than the key to insert
                // If it is not the lowest level, q is updated as the child node of the current node, r is the child node of the latest node, and if it is the lowest level, node of the current variable q is returned
                if(r ! =null) {
                    // Get the following index of the linked list: n
                    Node<K,V> n = r.node;
                    K k = n.key;
                    // If the value of this object is null, it indicates that the object has been deleted. The value of put cannot be null
                    // Unlink all upper-level indexes whose value is null
                    if (n.value == null) {
                        // drop the null index, that is, replace r with the subsequent index of r, and enter the inner for loop again
                        if(! q.unlink(r))break;           // restart
                        // r is set again to the right of the current index for the next inner loop
                        r = q.right;         // reread r
                        continue;
                    }
                    // If the key to be inserted is larger than the key of subsequent node N, skip to the next node
                    // q --> r r ---> r.right
                    // Until n's key is larger than the key to insert, breaking the loop
                    if (cpr(cmp, key, k) > 0) {
                        q = r;
                        r = r.right;
                        continue; }}// the index q is the lowest level index
                if ((d = q.down) == null)
                    return q.node;
                // If it is not the lowest index, assign q to the lower index of q,
                q = d;
                // r is assigned to the subsequent index qr = d.right; }}}Copy the code

The above figure shows the route to find the returned value, where the blue line is the pointing change of Q and the red line is the pointing change of R. Finally, the index at level 1 above key 2 meets the return condition and the Node with key 2 is returned.

Then there is the helpDelete method, which is executed only if (v = n.value) == NULL, twice to remove the node and replace n with a subsequent node of N, f.

1. Meet the f = = null | | f.v alue! = f execute and return, call this method to b --> n --> newNode<K,V>(f) --> f case 2. At this point in the next for loop, the else judgment will be satisfied, pointing NEXT of B to f, and n and next will not be in the listCopy the code
  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. */
            // If f is the subsequent points of the Node and the Node is the subsequent Node of b, the general situation must be satisfied, unless deleted in the middle
            if (f == next && this == b.next) {
                // If f is null, or the value of f is not f itself, create a new node and append the tag
                // execute casNext(f, new Node
      
       (f)); And then the structure is zero
      ,v>
                /* * b ---> n --- > new Node
      
       (f) --- > f * */
      ,v>
                if (f == null|| f.value ! = f)// not already marked
                    casNext(f, new Node<K,V>(f));
                else
                After executing the following statement, the structure is
                /* * b --> f n and new Node
      
       (f) * */
      ,v>
                    b.casNext(this, f.next); }}Copy the code

The image above shows the code after the first step

2. Create the node to be inserted

First, obtain the number of random layers and build a new Index from the bottom up. If the number of random layers is greater than the original maximum level, reconstruct headIndex.


        // Get a random number
        int rnd = ThreadLocalRandom.nextSecondarySeed();
        / / the random binary and binary 0 x80000001: with 10000000000000000000000000000001 operations
        // that is, the highest and lowest bits of the binary random number are both 0, and other bits are irrelevant. If not, the layer number of the node is not increased, and the third step is not continued
        if ((rnd & 0x80000001) = =0) { // test highest and lowest bits
            // The initial level is 1
            int level = 1, max;
            // Determine the number of consecutive 1s to the left of the binary from the last to the last of the random values
            while (((rnd >>>= 1) & 1) != 0)
                ++level;
            Index<K,V> idx = null;
            // header index h
            HeadIndex<K,V> h = head;
            // Max is assigned to the level of the header index, i.e., the highest level of the current skip table;
            // If level is smaller than Max
            if (level <= (max = h.level)) {
                // create the upper index of z. The upper index of z points to the new node, and then points to the lower index
                // There is no left or right association with skip lists
                for (int i = 1; i <= level; ++i)
                    idx = new Index<K,V>(z, idx, null);
            } else { // try to grow by one level
                // If level is greater than Max, set level = Max + 1
                level = max + 1; // hold in array and later pick the one to use
                // Create an array of length level + 1, since we want the index to start at 1, so + 1
                @SuppressWarnings("unchecked")Index<K,V>[] idxs =
                    (Index<K,V>[])newIndex<? ,? >[level+1];

                // create the upper index of z. The upper index of z points to the new node, and then points to the lower index
                // There is no left or right association with skip lists
                for (int i = 1; i <= level; ++i)
                    idxs[i] = idx = new Index<K,V>(z, idx, null);

                // Rebuild the header index
                for (;;) {
                    // h is reassigned to the header index, which is also the final header index
                    h = head;
                    // oldLevel is assigned to the original level
                    int oldLevel = h.level;
                    // If level is equal to oldLevel, another thread has changed the level of the header loop. for
                    if (level <= oldLevel) // lost race to add level
                        break;
                    // Reset a header index
                    HeadIndex<K,V> newh = h;
                    // Get the node of the header index
                    Node<K,V> oldbase = h.node;
                    // Create a header index through a loop, usually only once
                    for (int j = oldLevel+1; j <= level; ++j)
                        newh = new HeadIndex<K,V>(oldbase, newh, idxs[j], j);
                    // cas replaces h with newh for head, not h = newh
                    // There is no need to build the pointing relationship again because there is only a header index and a new node index on the new chain surface
                    /* Successful head Head node status: head ↓ Original head */
                    if (casHead(h, newh)) {
                        // assign h to newh
                        h = newh;
                        // set level to the level of the original header index, and then set idx to the index at level of idxs
                        idx = idxs[level = oldLevel];
                        break; }}}Copy the code

After the execution of the second step, suppose that, level is 4, the skip list structure is as shown in the figure above. The changes are as follows:

2. Create a new headIndex 3. The newly created headIndex right points to the top-level index of the newly created node, and the top-level index right points to null 4. The newly created indexes all point down in orderCopy the code
3. Construct the pointing relationship of Index added in each layer

The main logic is to compare right from the highest level to the original level, find the appropriate position, build the correct pointing relationship, and then start to build the next level.

   			// insertionLevel: the current number of levels to build the pointing relationship, starting with the level of the original header
            splice: for (int insertionLevel = level;;) {
                // j: the initial level is the current level of the header index
                int j = h.level;
                Q -> q.ight -> q.ight. Right...
                R -> r.ight -> r.ight. Right...
                // t: new index at the level where q resides
                // Build the pointer relationship of each new node in a loop, starting with the initial index, until the key that needs to be inserted is smaller than that of an index
                for (Index<K,V> q = h, r = q.right, t = idx;;) {
                    // If q or t is null, the other thread deleted q or t and re-entered for (;;).
                    if (q == null || t == null)
                        break splice;
                    // if r is empty, it is the maximum value of this layer
                    if(r ! =null) {
                        // n Node assigned to r
                        Node<K,V> n = r.node;
                        // compare before deletion check avoids needing recheck
                        // Compare the key to be inserted with n's key
                        int c = cpr(cmp, key, n.key);
                        // Put does not allow the value to be null. If value is null, the index has been deleted
                        if (n.value == null) {
                            if(! q.unlink(r))break;
                            r = q.right;
                            continue;
                        }
                        // If we insert a key greater than n, proceed further
                        if (c > 0) {
                            q = r;
                            r = r.right;
                            continue; }}// If the number of levels in the current header index is the same as the number of levels in the original header index, the number of levels has not changed
                    if (j == insertionLevel) {
                        For (int insertionLevel = level;;)
                        if(! q.link(r, t))break; // restart
                        For (int insertionLevel = level;;) if the value of the new node is null, the node has been deleted by another thread.
                        if (t.node.value == null) {
                            findNode(key);
                            break splice;
                        }
                        // Exit the loop at the bottom layer to complete the PUT operation
                        if (--insertionLevel == 0)
                            break splice;
                    }

                    // Q, r, t move down as the number of node layers move down to prepare for the lower layer construction operation
                    if(--j >= insertionLevel && j < level) t = t.down; q = q.down; r = q.right; }}}return null;
    }
Copy the code

There is a findNode(key) method, annotated here

private Node<K,V> findNode(Object key) {
        if (key == null)
            throw new NullPointerException(); // don't postpone errors
        Comparator<? super K> cmp = comparator;
        outer: for (;;) {
            // Find the front node b of the target node, where n is the rear node of b
            for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
                Object v; int c;
                // If the back-end node is null, no further action is required
                if (n == null)
                    break outer;
                // Get subsequent nodes of subsequent nodes
                Node<K,V> f = n.next;
                // If the node in position n is a subsequent node, it has been deleted, go to the next for (;;).
                if(n ! = b.next)// inconsistent read
                    break;
                // If the value of node N is null, it indicates that node N has been deleted. Set next of node B to f, and enter next for (;;).
                if ((v = n.value) == null) {    // n is deleted
                    n.helpDelete(b, f);
                    break;
                }
                // If the value of front-end node B is null, it has been deleted. Go to for (;) next time.
                if (b.value == null || v == n)  // b is deleted
                    break;
                // If the target key is equal to n.key, return the subsequent node n
                if ((c = cpr(cmp, key, n.key)) == 0)
                    return n;
                // If it is larger than the key of the subsequent node, advance backward
                if (c < 0)
                    breakouter; b = n; n = f; }}return null;
    }
Copy the code

At this point, the skip list put is complete.Then look at the get method, the overall process is as follows:

1. Call the findPredecessor method and start searching from the right of the de novo Index. If the node key of the subsequent Index is larger than the key we want to search, the head Index is moved downward and searched in the lower Index until no lower Index is found. 3. Start from the nodes found by the findBody predecessor method and go right. Returns the result until the key of a node is equal to the key of the target, or null if the key is less than the target keyCopy the code
 public V get(Object key) {
        return doGet(key);
    }
Copy the code
   private V doGet(Object key) {
        if (key == null)
            throw new NullPointerException();
        Comparator<? super K> cmp = comparator;
        outer: for (;;) {
            /* * b: the node traversed currently, which is initially the front node of the newly inserted node * n: is the next node traversed currently, which is initially the rear node of b before the new node is inserted * */
            for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
                Object v; int c;
                if (n == null)
                    break outer;
                // f: n after node
                Node<K,V> f = n.next;
                // If n is not a subsequent node of b, do the next for (;;). , possibly because other threads have inserted other nodes into subsequent nodes of B
                if(n ! = b.next)// inconsistent read
                    break;
                // If the value of subsequent node n is null, it indicates that the node has been deleted, then delete the node, replace n with the subsequent node f of n, the next for (;;).
                if ((v = n.value) == null) {    // n is deleted
                    n.helpDelete(b, f);
                    break;
                }
                // If the value of b is null, it is deleted by another thread. For (;;)
                if (b.value == null || v == n)  // b is deleted
                    break;
                // The key you want to find is the same as the key of n
                if ((c = cpr(cmp, key, n.key)) == 0) {
                    @SuppressWarnings("unchecked") V vv = (V)v;
                    return vv;
                }
                // If less than, exit the for loop and return null
                if (c < 0)
                    break outer;
                // If the key to be inserted is larger than that of the subsequent node, proceed to the subsequent nodeb = n; n = f; }}return null;
    }
Copy the code

The last one is the remove method, which needs to be explained here. Because ConcurrentSkipListMap supports concurrency, when a node is deleted, it may be inserted by another thread. Therefore, a special node will be added after the node to mark the node to be deleted before it is deleted. Resolve a problem where new data is added after being deleted, and then the data is deleted. The remove method is mainly divided into three steps:

1. FindPredecessor method to obtain suitable front node B 2. Then, a series of concurrent CAS operations are performed. Then, the key of N is compared with the key to be deleted. If the key to be deleted is larger than that of N, the process continues. If the keys are equal, set the value of n to null first, and then point B to N.next, indicating that N has been deleted in the linked list of nodes. Then findPredecessor is called to delete the indexes of deleted node N on each index layer.Copy the code
   public V remove(Object key) {
        return doRemove(key, null);
    }
Copy the code
final V doRemove(Object key, Object value) {
        if (key == null)
            throw new NullPointerException();
        Comparator<? super K> cmp = comparator;
        outer: for (;;) {
            /* * Or findPredecessor method * b: the node currently traversed, initially the front node of the newly inserted node * n: is the next node currently traversed, initially the rear node of b before the new node is inserted * */
            for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
                Object v; int c;
                // If n is null, it indicates that it has been deleted and exits the loop
                if (n == null)
                    break outer;
                // f: subsequent nodes of n
                Node<K,V> f = n.next;
                // n is not a subsequent node of b, read inconsistent, proceed next for (;;)
                if(n ! = b.next)// inconsistent read
                    break;
                // If the value of n is null, the node is marked or deleted
                if ((v = n.value) == null) {        // n is deleted
                    n.helpDelete(b, f);
                    break;
                }
                // indicates that another thread deleted b, or has been marked for deletion, go to the next for (;;).
                // The helpDelete method marks to be deleted. If the correlation is as follows, it marks to be deleted
                / / left
                // Node to be removed --> new Node
      
       (subsequent Node of Node) --> Original successor Node of Node to be removed
      ,v>
                if (b.value == null || v == n)      // b is deleted
                    break;
                // Compare the target key with n's key
                // If the key is smaller than N, there is no corresponding node. No further operation is required
                if ((c = cpr(cmp, key, n.key)) < 0)
                    break outer;
                // The value greater than 0 is traversed through subsequent nodes
                if (c > 0) {
                    b = n;
                    n = f;
                    continue;
                }
                // The following is the equivalent operation
                // value ! = null Indicates whether the values are the same
                if(value ! =null && !value.equals(v))
                    break outer;
                // Set the value of n to null, not yet removed from the list, continue next time for (;;)
                if(! n.casValue(v,null))
                    break;
                // appendMarker is an additional marker that marks n to be deleted and the correlation pointer is b --> n --> new Node
      
       (f) --> f
      ,v>
                // Then the next node of b points to f. There are no nodes in the skip list that point to N, but the Index of N is still in the skip list
                if(! n.appendMarker(f) || ! b.casNext(n, f)) findNode(key);// retry via findNode
                else {
                    // This method is used not only to find the appropriate front node, but also to unlink the null index
                    // the index n is removed from the skip list
                    findPredecessor(key, cmp);      // clean index
                    if (head.right == null)
                        tryReduceLevel();
                }
                @SuppressWarnings("unchecked") V vv = (V)v;
                returnvv; }}return null;
    }
Copy the code

conclusion

Because ConcurrentSkipListMap stores key-value pairs, Node is used to store data and form a complete linked list, not a skip list structure. Skip lists are mainly realized through Index, each Index has a Node to point to, that is, Index as an Index, is used to speed up the query efficiency, Node is the real storage of data. ConcurrentSkipListMap is very efficient. If the application needs to be ordered, then hop tables are a good choice. If the application needs to be ordered, hop tables are a good choice.