J.U.C 之 ConcurrentSkipListMap

So far, we see two key-value data structure to achieve in the Java world: Hash, TreeMap, these two data structures each have advantages and disadvantages.

  1. Hash table: insert, to find the fastest, is O (1); implemented as linked lists can be achieved without locking; ordered data requires an explicit sort operation.
  2. Red-black tree: insert, lookup is O (logn), but smaller constant term; lock-complexity implementation of high, generally requires lock; natural order data. However, this introduces a third implementation of key-value data structure: SkipList. SkipList with the efficiency of not less than red-black tree, but its principles and implementation complexity is much simpler than a red-black tree.

SkipList

What is SkipList? Skip List, called jump table, which is an alternative to the balanced tree data structure, the data elements in accordance with the default key values ​​in ascending order, the natural order. Skip list so that the sorted data in a multi-layer distribution list, the random number 0-1 decision whether or not to climb up one of the data, the "space for time" of an algorithm that increases the forward in each node pointer, inserting, deleting, you can ignore some nodes can not be involved when looking to improve efficiency.

We look at a simple linked list, as follows:

If we need to query 9,21,30, you need to compare the number of 3 + 6 + 8 = 17 times, then there is no plan to optimize it? Have! We extract some of the elements of the list out as a more "index", as follows:

We first carried out a comparison to determine right or element is laid down with these indexes, due to the existence of "index" reasons, resulting in retrieval time will greatly reduce the number of comparisons. Of course, many elements are not hard to reflect the advantage, when the element enough time, this index structure will flourish.

The characteristics SkipList

SkipList has the following characteristics:

  1. Structures composed of many layers, level by a certain probability of randomly generated
  2. Each layer is an ordered list, the default is ascending, can be sorted according Comparator provided at map creation, depending on construction methods used
  3. The lowest level (Level 1) that contains all the elements of the list
  4. If an element appears in the list of Level i, then it lists under Level i also will appear
  5. Each node contains two pointers, a pointer to the next element in the same list, a pointer to one of the following elements

We will do some extensions on the map can become a typical structure of SkipList

Find SkipList

SkipListd search algorithm is relatively simple, we we're looking for the top element 21, the process is as follows:

  1. Comparison 3, more than 3, looking back (9),
  2. 9 is larger than continue looking back (25), but less than 25, down from 9 to start looking for one (16)
  3. 16 still behind node 25, will continue to look for one from 16
  4. Found 21

Red dotted line represents the path.

SkipList insert

SkipList inserting operation include:

  1. Find a suitable location. It should be clear that when a new node to confirm the level occupied by K, by way of lost coin, completely random. If K is greater than the level occupied by the list level, then re-apply for a new layer, or insert the specified level
  2. Apply for a new node
  3. Adjust pointer

Suppose we want to insert elements 23, through the search can confirm that she is located 25 before and after 9,16,21. Of course, we need to consider the application level K.

If the level of K> 3

Need to apply for a new level (Level 4)

If the level of K = 2

Level 2 can be inserted directly in the layer

Here at involves algorithms: K levels by throwing a coin, we have to analyze the algorithms behind ConcurrentSkipListMap by source. There is also a place to note is that, after the K layer insert elements, the need to ensure that all levels of less than K layer should be the emergence of new nodes.

Delete the SkipList

Insert and delete nodes nodes are basically the same idea: to find nodes, delete nodes, adjust the pointer.

For example, to delete a node 9, as follows:

ConcurrentSkipListMap

Through the above we know SkipList algorithm using space for time, find its insertion and efficiency of O (logn), the efficiency of not less than red-black tree, but its principles and implementation complexity is much simpler than a red-black tree. In general operation will list List, it will be on SkipList no pressure.

ConcurrentSkipListMap its internal data structures using SkipLis achieved. To achieve SkipList, ConcurrentSkipListMap provided three internal classes to construct such a list structure: Node, Index, HeadIndex. Wherein the bottommost single Node ordered linked list node, Index is expressed as an index based on the Node layer, HeadIndex to maintain the index level. When here we can say that ConcurrentSkipListMap by HeadIndex maintain the index level, look down from the topmost layer by the beginning Index, step by step to narrow your search, finally reaching the lowest level Node, we only need a relatively small part of the data. JDK in relation below:

Node

static final class Node<K,V> {
    final K key;
    volatile Object value;
    volatile ConcurrentSkipListMap.Node<K, V> next;

    /** 省略些许代码 */
}
复制代码

Node structure of a general single chain and there is no difference, key-value and a pointer to the next node in the next.

Index

static class Index<K,V> {
    final ConcurrentSkipListMap.Node<K,V> node;
    final ConcurrentSkipListMap.Index<K,V> down;
    volatile ConcurrentSkipListMap.Index<K,V> right;

    /** 省略些许代码 */
}
复制代码

Index provides an index based on a Node Node node, a pointer to the next Index right, pointing to a lower node down.

HeadIndex

static final class HeadIndex<K,V> extends Index<K,V> {
    final int level;  //索引层,从1开始,Node单链表层为0
    HeadIndex(Node<K,V> node, Index<K,V> down, Index<K,V> right, int level) {
        super(node, down, right);
        this.level = level;
    }
}
复制代码

Internal HeadIndex on a level defined hierarchy.

ConcurrentSkipListMap provides four constructors, and each constructor calls the initialize () method for initialization.

final void initialize() {
    keySet = null;
    entrySet = null;
    values = null;
    descendingMap = null;
    randomSeed = seedGenerator.nextInt() | 0x0100; // ensure nonzero
    head = new ConcurrentSkipListMap.HeadIndex<K,V>(new ConcurrentSkipListMap.Node<K,V>(null, BASE_HEADER, null),
            null, null, 1);
}
复制代码

Note that, the initialize () method not only is called in the constructor, such as clone, clear, when the method is invoked readObject initialization step. It should be noted randomSeed initialization.

private transient int randomSeed;
randomSeed = seedGenerator.nextInt() | 0x0100; // ensure nonzero
复制代码

randomSeed a simple random number generator (described later).

put operation

Providing CoucurrentSkipListMap put () method is used to associate the specified value with the specified key in this map. Source as follows:

public V put(K key, V value) {
    if (value == null)
        throw new NullPointerException();
    return doPut(key, value, false);
}
复制代码

First, determine if the value is null, throw NullPointerException, or call doPut method, in fact, if you read the JDK source code, it should operate on this very familiar, JDK source code there are many ways to do some of the need for verification after then the true method by calling do ** ().

doPut () method more content, we step by step analysis.

private V doPut(K key, V value, boolean onlyIfAbsent) {
    Node<K,V> z;             // added node
    if (key == null)
        throw new NullPointerException();
    // 比较器
    Comparator<? super K> cmp = comparator;
    outer: for (;;) {
        for (Node<K, V> b = findPredecessor(key, cmp), n = b.next; ; ) {

        /** 省略代码 */
复制代码

doPut () method has three parameters, in addition to the key, value as well as a boolean onlyIfAbsent, the parameter function and if there is a current key, do what action. When onlyIfAbsent is false, the replacement value, is true, the value is returned. Explained by the code:

 if (!map.containsKey(key))
    return map.put(key, value);
else
     return map.get(key);
复制代码

First, determine whether the key is null, if null, then throw NullPointerException, from here we can confirm ConcurrentSkipList does not support the key or value is null . Then call findPredecessor () method, passing key to confirm the location. findPredecessor () method is actually to confirm the key inserted.

private Node<K,V> findPredecessor(Object key, Comparator<? super K> cmp) {
     if (key == null)
         throw new NullPointerException(); // don't postpone errors
     for (;;) {
         // 从head节点开始,head是level最高级别的headIndex
         for (Index<K,V> q = head, r = q.right, d;;) {

             // r != null,表示该节点右边还有节点,需要比较
             if (r != null) {
                 Node<K,V> n = r.node;
                 K k = n.key;
                 // value == null,表示该节点已经被删除了
                 // 通过unlink()方法过滤掉该节点
                 if (n.value == null) {
                     //删掉r节点
                     if (!q.unlink(r))
                         break;           // restart
                     r = q.right;         // reread r
                     continue;
                 }

                 // value != null,节点存在
                 // 如果key 大于r节点的key 则往前进一步
                 if (cpr(cmp, key, k) > 0) {
                     q = r;
                     r = r.right;
                     continue;
                 }
             }

             // 到达最右边,如果dowm == null,表示指针已经达到最下层了,直接返回该节点
             if ((d = q.down) == null)
                 return q.node;
             q = d;
             r = d.right;
         }
     }
 }
复制代码

findPredecessor () method meaning is clear: to find the older generation. HeadIndex start from the top right comparative step by step, until the right is the key null Node or right node becomes greater than the current key, and then looking down, followed by repeating the process until far down as null, that is, to find the older generation, Note that to see the results returned by Node, not the Item, so the location should be inserted into the bottom of the Node list.

In this process, the method gives a ConcurrentSkipListMap other functions, is by determining whether the node's value is null, if null, indicating that the node has been deleted, remove the node by calling unlink () method.

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

To delete a node is a simple process to change the pointer to the lower right.

After () to find the older generation nodes findPredecessor, to do what? look down:

for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
       // 前辈节点的next != null
       if (n != null) {
           Object v; int c;
           Node<K,V> f = n.next;

           // 不一致读,主要原因是并发,有节点捷足先登
           if (n != b.next)               // inconsistent read
               break;

           // n.value == null,该节点已经被删除了
           if ((v = n.value) == null) {   // n is deleted
               n.helpDelete(b, f);
               break;
           }

           // 前辈节点b已经被删除
           if (b.value == null || v == n) // b is deleted
               break;

           // 节点大于,往前移
           if ((c = cpr(cmp, key, n.key)) > 0) {
               b = n;
               n = f;
               continue;
           }

           // c == 0 表示,找到一个key相等的节点,根据onlyIfAbsent参数来做判断
           // onlyIfAbsent ==false,则通过casValue,替换value
           // onlyIfAbsent == true,返回该value
           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
       }

       // 将key-value包装成一个node,插入
       z = new Node<K,V>(key, value, n);
       if (!b.casNext(n, z))
           break;         // restart if lost race to append to b
       break outer;
   }
复制代码

After finding a suitable location, the node is inserted at the position slightly. The insertion node process is relatively simple, is the key-value packed into a Node, and then added to the list the method by which casNext (). Of course, it is the need for a series of validation work prior to insertion.

After the lowermost insert node, what next step is? New Index. Front bloggers mentioned, when the insert node, the new node will be determined according to the level of the inserted coin toss embodiment employed, since there may be concurrent, a ConcurrentSkipListMap ThreadLocalRandom employed to generate random numbers. as follows:

int rnd = ThreadLocalRandom.nextSecondarySeed();
复制代码

Flip a coin to level the idea is simply to flip a coin if the coin is the positive level level + 1, otherwise stop, as follows:

// 抛硬币决定层次
while (((rnd >>>= 1) & 1) != 0)
    ++level;
复制代码

When a node is inserted elaborate SkipList explained, the decision level level will be divided into two cases are processed, one level if the level is greater than the maximum level, then you need to add a layer, or to the appropriate level and less than the level in hierarchically new process node.

level <= headIndex.level

// 如果决定的层次level比最高层次head.level小,直接生成最高层次的index
// 由于需要确认每一层次的down,所以需要从最下层依次往上生成
if (level <= (max = h.level)) {
    for (int i = 1; i <= level; ++i)
        idx = new ConcurrentSkipListMap.Index<K,V>(z, idx, null);
}
复制代码

Starting from the bottom, is smaller than a level of the index of each layer is initialized, each of the node point to the newly added node, down, Item point of the next layer, right next to all null. The entire process is very simple: each layer is less than the initialization of a level index, then added to the original index to the chain.

level > headIndex.level

// leve > head.level 则新增一层
 else { // try to grow by one level
     // 新增一层
     level = max + 1;

     // 初始化 level个item节点
     @SuppressWarnings("unchecked")
     ConcurrentSkipListMap.Index<K,V>[] idxs =
             (ConcurrentSkipListMap.Index<K,V>[])new ConcurrentSkipListMap.Index<?,?>[level+1];
     for (int i = 1; i <= level; ++i)
         idxs[i] = idx = new ConcurrentSkipListMap.Index<K,V>(z, idx, null);

     //
     for (;;) {
         h = head;
         int oldLevel = h.level;
         // 层次扩大了,需要重新开始(有新线程节点加入)
         if (level <= oldLevel) // lost race to add level
             break;
         // 新的头结点HeadIndex
         ConcurrentSkipListMap.HeadIndex<K,V> newh = h;
         ConcurrentSkipListMap.Node<K,V> oldbase = h.node;
         // 生成新的HeadIndex节点,该HeadIndex指向新增层次
         for (int j = oldLevel+1; j <= level; ++j)
             newh = new ConcurrentSkipListMap.HeadIndex<K,V>(oldbase, newh, idxs[j], j);

         // HeadIndex CAS替换
         if (casHead(h, newh)) {
             h = newh;
             idx = idxs[level = oldLevel];
             break;
         }
     }
复制代码

When the level flip a coin larger than the maximum level level, we need to add a layer of processing. Processing logic is as follows:

  1. Initializing an array index corresponding to a size of level + 1, and then creates an index for each unit, as a parameter: Node is new Z, down to the next layer index, right is null
  2. Expansion operation is performed by a for loop. From the highest level process, add a HeadIndex, two parameters: Node Node, down are the highest level of Node and HeadIndex, right to the corresponding level of index just created, level hierarchical level corresponding. Finally, the head and the head of the new current layer is added by replacing the CAS. Through the above steps, we found that although has found a senior node will also be node is inserted, is also determined to determine the level and generate the corresponding Index, but not these Index into the corresponding level which, so the following code is to inserted into the corresponding index among layers.
// 从插入的层次level开始
  splice: for (int insertionLevel = level;;) {
      int j = h.level;
      //  从headIndex开始
      for (ConcurrentSkipListMap.Index<K,V> q = h, r = q.right, t = idx;;) {
          if (q == null || t == null)
              break splice;

          // r != null;这里是找到相应层次的插入节点位置,注意这里只横向找
          if (r != null) {
              ConcurrentSkipListMap.Node<K,V> n = r.node;

              int c = cpr(cmp, key, n.key);

              // n.value == null ,解除关系,r右移
              if (n.value == null) {
                  if (!q.unlink(r))
                      break;
                  r = q.right;
                  continue;
              }

              // key > n.key 右移
              if (c > 0) {
                  q = r;
                  r = r.right;
                  continue;
              }
          }

          // 上面找到节点要插入的位置,这里就插入
          // 当前层是最顶层
          if (j == insertionLevel) {
              // 建立联系
              if (!q.link(r, t))
                  break; // restart
              if (t.node.value == null) {
                  findNode(key);
                  break splice;
              }
              // 标志的插入层 -- ,如果== 0 ,表示已经到底了,插入完毕,退出循环
              if (--insertionLevel == 0)
                  break splice;
          }

          // 上面节点已经插入完毕了,插入下一个节点
          if (--j >= insertionLevel && j < level)
              t = t.down;
          q = q.down;
          r = q.right;
      }
  }
复制代码

This code is divided into two parts to see part of the hierarchy to find the corresponding insertion node position, the second portion is inserted in this position, and then down.

At this point, ConcurrentSkipListMap's put this operation is over. The amount of code a little more than summarize here:

  1. First () method to find the older generation nodes Node by findPredecessor
  2. The predecessor node and returned key-value, the new node Node, while the next set by a CAS
  3. Setting Node Node, and then sets the inode. Take a coin toss decided the level, if the decision level is greater than the existing maximum level, then add a layer, then create a new Item list.
  4. Finally, the list inserted into the new Item SkipList structure.

get operation

Compared to put the operation, get the operation will be made easier, the process is in fact only the first step of the operation put the equivalent of:

private V doGet(Object key) {
      if (key == null)
          throw new NullPointerException();
      Comparator<? super K> cmp = comparator;
      outer: for (;;) {
          for (ConcurrentSkipListMap.Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
              Object v; int c;
              if (n == null)
                  break outer;
              ConcurrentSkipListMap.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;
  }
复制代码

And put the operation is similar to the first step, first call findPredecessor () method to find the predecessor node, and then down the right has the right to look for, and bear the same delete a node value is null responsibilities in this process.

remove operation

remove deletes the specified key nodes as follows:

public V remove(Object key) {
    return doRemove(key, null);
}
复制代码

Direct call doRemove () method, there are two parameters remove, is a key, is another value, i.e., it provides a method doRemove remove key, it is also provided satisfy key-value.

final V doRemove(Object key, Object value) {
       if (key == null)
           throw new NullPointerException();
       Comparator<? super K> cmp = comparator;
       outer: for (;;) {
           for (ConcurrentSkipListMap.Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
               Object v; int c;
               if (n == null)
                   break outer;
               ConcurrentSkipListMap.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节点已删除
               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;
               }

               /*
                * 找到节点
                */

               // value != null 表示需要同时校验key-value值
               if (value != null && !value.equals(v))
                   break outer;

               // CAS替换value
               if (!n.casValue(v, null))
                   break;
               if (!n.appendMarker(f) || !b.casNext(n, f))
                   findNode(key);                  // retry via findNode
               else {
                   // 清理节点
                   findPredecessor(key, cmp);      // clean index

                   // head.right == null表示该层已经没有节点,删掉该层
                   if (head.right == null)
                       tryReduceLevel();
               }
               @SuppressWarnings("unchecked") V vv = (V)v;
               return vv;
           }
       }
       return null;
   }
复制代码

Call findPredecessor () method to find the predecessor node, then right, then compared, after finding a CAS replace the value is null, then it is determined that the node is not the only index layer, if so, calling tryReduceLevel () method of this layer kill, to complete the removal.

In fact, it can be seen from here, remove only method is to set the value of Node null, does not really remove the node Node, in fact, from the above put operation, get the operation we can see that they are looking for will determine when node node whether the value is null, if null, then call unLink () method to cancel the association, as follows:

if (n.value == null) {
    if (!q.unlink(r))
        break;           // restart
    r = q.right;         // reread r
    continue;
}
复制代码

size operation

ConcurrentSkipListMap of different size () operation and ConcurrentHashMap, it does not maintain a global variable to count the number of elements, so every time the method is called when they are needed to traverse.

public int size() {
    long count = 0;
    for (Node<K,V> n = findFirst(); n != null; n = n.next) {
        if (n.getValidValue() != null)
            ++count;
    }
    return (count >= Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int) count;
}
复制代码

Call findFirst () method to find the first Node, and then use the next node to count. Finally, return to statistics, up to return Integer.MAX_VALUE. Note that in the thread concurrency is safe.

Guess you like

Origin juejin.im/post/5d9beab85188251d805f3f6c