JUC:ConcurrentSkipListMap/ConcurrentSkipListSet(并发容器)

JUC:ConcurrentSkipListMap/ConcurrentSkipListSet(并发容器)


关键词

  • 无锁链表(无锁地实现节点的增加、删除,只在队头、队尾进行CAS操作)
  • 并发问题,删除(分两步,在1个CAS操作),Mark节点(删除节点的next指针指向该Marker节点)
  • 定位位置;检查,执行了一系列的检查逻辑(检测删除);执行具体操作

ConcurrentHashMap 是一种 key 无序的 HashMap
ConcurrentSkipListMap则是 key 有序的 TreeMap,实现了NavigableMap接口,此接口又继承了SortedMap接口

一、ConcurrentSkipListMap

1.1 为什么要使用SkipList实现Map?

在Java的util包中,有一个非线程安全的HashMap,也就是TreeMap,是key有序的,基于红黑树实现。
而在Concurrent包中,提供的key有序的HashMap,也就是ConcurrentSkipListMap,是基于SkipList(跳查表)来实现的。这里为什么不用红黑树,而用跳查表来实现呢?

借用Doug Lea的原话:

The reason is that there are no known efficient lock0free insertion and deletion algorithms for search trees.

也就是目前计算机领域还未找到一种高效的、作用在树上的、无锁的、增加和删除节点的办法
那为什么SkipList可以无锁地实现节点的增加、删除呢?这要从无锁链表的实现说起。

1.2 无锁链表(无锁地实现节点的增加、删除)(只在队头、队尾进行CAS操作)

无锁队列、栈,都是只在队头、队尾进行CAS操作,通常不会有问题。如果在链表的中间进行插入或删除操作,按照通常的CAS做法,就会出现问题!

关于这个问题,Doug Lea的论文中有清晰的论述,此处引用如下:

  • 操作1:在节点10后面插入节点20。如下图所示,首先把节点20的next指针指向节点30,然后对节点10的next指针执行CAS操作,使其指向节点20即可。
    在这里插入图片描述
  • 操作2:删除节点10。如下图所示,只需把头节点的next指针,进行CAS操作到节点30即可。
    在这里插入图片描述但是,如果两个线程同时操作,一个删除节点10,一个要在节点10后面插入节点20。并且这两个操作都各自是CAS的,此时就会出现问题。如下图所示,删除节点10,会同时把新插入的节点20也删除掉!这个问题超出了CAS的解决范围

在这里插入图片描述
为什么会出现这个问题呢?

究其原因:在删除节点10的时候,实际受到操作的是节点10的前驱,也就是头节点。节点10本身没有任何变化。故而,再往节点10后插入节点20的线程,并不知道节点10已经被删除了

针对这个问题,在论文中提出了如下的解决办法,如下图所示,把节点 10 的删除分为两2步:

  • 第一步,把节点10的next指针,mark标成删除,即软删除;

  • 第二步,找机会,物理删除。

    做标记之后,当线程再往节点10后面插入节点20的时候,便可以先进行判断,节点10是否已经被删
    除,从而避免在一个删除的节点10后面插入节点20。

    这个解决方法有一个关键点: “把节点10的next指针指向节点20(插入操作)” 和 “判断节点10本身是否已经删除(判断操作)”,必须是原子的,必须在 1个CAS操作 里面完成!
    在这里插入图片描述
    具体的实现有两个办法:

  • 办法1:AtomicMarkableReference(效率低)

保证每个 next 是 AtomicMarkableReference 类型。但这个办法不够高效,Doug Lea 在ConcurrentSkipListMap的实现中用了另一种办法。

  • 办法2:Mark节点()

我们的目的是标记节点10已经删除,也就是标记它的next字段。那么可以新造一个marker节点,使节点10的next指针指向该Marker节点。这样,当向节点10的后面插入节点20的时候,就可以在插入的同时判断节点10的next指针是否指向了一个Marker节点,这两个操作(判断和添加操作)可以在一个CAS操作里面完成。

1.3 跳查表

解决了无锁链表的插入或删除问题,也就解决了跳查表的一个关键问题。因为跳查表就是多层链表叠起来的。

下面先看一下跳查表的数据结构(下面所用代码都引用自JDK 7,JDK 8中的代码略有差异,但不影响下面的原理分析)。
在这里插入图片描述上图中的Node就是跳查表底层节点类型。所有的<K, V>对都是由这个单向链表串起来的。

上面的Index层的节点:
在这里插入图片描述上图中的node属性不存储实际数据,指向Node节点。
down属性:每个Index节点,必须有一个指针,指向其下一个Level对应的节点。

right属性:Index也组成单向链表。

整个ConcurrentSkipListMap就只需要记录顶层的head节点即可:

public class ConcurrentSkipListMap<K,V> extends AbstractMap<K,V> implements ConcurrentNavigableMap<K,V>, Cloneable, Serializable {
  // ...
  private transient Index<K,V> head;
  // ...
}

在这里插入图片描述下面详细分析如何从跳查表上查找、插入和删除元素。

  1. put实现分析
    在这里插入图片描述
private V doPut(K key, V value, boolean onlyIfAbsent) {
  if (key == null)
    throw new NullPointerException();
  Comparator<? super K> cmp = comparator;
  for (;;) {
    Index<K,V> h; Node<K,V> b;
    VarHandle.acquireFence();
    int levels = 0;           // number of levels descended
    if ((h = head) == null) {      // 初始化
      Node<K,V> base = new Node<K,V>(null, null, null);
      h = new Index<K,V>(base, null, null);
      b = (HEAD.compareAndSet(this, null, h)) ? base : null;
   }
    else {
      for (Index<K,V> q = h, r, d;;) { // count while descending
        while ((r = q.right) != null) {
          Node<K,V> p; K k;
          if ((p = r.node) == null || (k = p.key) == null ||
            p.val == null)
            RIGHT.compareAndSet(q, r, r.right);
          else if (cpr(cmp, key, k) > 0)
            q = r;
          else
            break;
       }
        if ((d = q.down) != null) {
          ++levels;
          q = d;
       }
        else {
          b = q.node;
          break;
       }
     }
   }
    if (b != null) {
      Node<K,V> z = null;        // new node, if inserted
      for (;;) {            // find insertion point
        Node<K,V> n, p; K k; V v; int c;
        if ((n = b.next) == null) {
          if (b.key == null)    // if empty, type check key now
            cpr(cmp, key, key);
          c = -1;
       }
        else if ((k = n.key) == null)
          break;          // can't append; restart
        else if ((v = n.val) == null) {
          unlinkNode(b, n);
          c = 1;
       }
        else if ((c = cpr(cmp, key, k)) > 0)
          b = n;
        else if (c == 0 &&
            (onlyIfAbsent || VAL.compareAndSet(n, v, value)))
          return v;
        if (c < 0 &&
          NEXT.compareAndSet(b, n,
                   p = new Node<K,V>(key, value, n))) {
          z = p;
          break;
       }
     }
      if (z != null) {
        int lr = ThreadLocalRandom.nextSecondarySeed();
        if ((lr & 0x3) == 0) {    // add indices with 1/4 prob
          int hr = ThreadLocalRandom.nextSecondarySeed();
          long rnd = ((long)hr << 32) | ((long)lr & 0xffffffffL);
          int skips = levels;    // levels to descend before add
          Index<K,V> x = null;
          for (;;) {        // create at most 62 indices
            x = new Index<K,V>(z, x, null);
            if (rnd >= 0L || --skips < 0)
              break;      
            else
              rnd <<= 1;
         }
          if (addIndices(h, skips, x, cmp) && skips < 0 &&
            head == h) {     // try to add new level
            Index<K,V> hx = new Index<K,V>(z, x, null);
            Index<K,V> nh = new Index<K,V>(h.node, h, hx);
            HEAD.compareAndSet(this, h, nh);
         }
          if (z.val == null)    // deleted while adding indices
            findPredecessor(key, cmp); // clean
       }
        addCount(1L);
        return null;
     }
   }
 }
}      
      
      

在底层,节点按照从小到大的顺序排列,上面的index层间隔地串在一起,因为从小到大排列。查找的时候,从顶层index开始自左往右、自上往下,形成图示的遍历曲线。假设要查找的元素是32,遍历过程如下:

  • 先遍历第2层Index,发现在21的后面;
  • 从21下降到第1层Index,从21往后遍历,发现在21和35之间;
  • 从21下降到底层,从21往后遍历,最终发现在29和35之间。

在整个的查找过程中,范围不断缩小,最终定位到底层的两个元素之间。

在这里插入图片描述
关于上面的put(…)方法,有一个关键点需要说明:在通过findPredecessor找到了待插入的元素在[b,n]之间之后,并不能马上插入。因为其他线程也在操作这个链表,b、n都有可能被删除,所以在插入之前执行了一系列的检查逻辑,而这也正是无锁链表的复杂之处。

  1. remove(…)分析

在这里插入图片描述

// 若找到了(key, value)就删除,并返回value;找不到就返回null
final V doRemove(Object key, Object value) {
  if (key == null)
    throw new NullPointerException();
  Comparator<? super K> cmp = comparator;
  V result = null;
  Node<K,V> b;
  outer: while ((b = findPredecessor(key, cmp)) != null &&
         result == null) {
    for (;;) {
      Node<K,V> n; K k; V v; int c;
      if ((n = b.next) == null)
        break outer;
      else if ((k = n.key) == null)
        break;
      else if ((v = n.val) == null)
        unlinkNode(b, n);
      else if ((c = cpr(cmp, key, k)) > 0)
        b = n;
      else if (c < 0)
        break outer;
      else if (value != null && !value.equals(v))
        break outer;
      else if (VAL.compareAndSet(n, v, null)) {
        result = v;
        unlinkNode(b, n);
        break; // loop to clean up
     }
   }
 }
  if (result != null) {
    tryReduceLevel();
    addCount(-1L);
 }
  return result;
}

上面的删除方法和插入方法的逻辑非常类似,因为无论是插入,还是删除,都要先找到元素的前驱,也就是定位到元素所在的区间[b,n]。在定位之后,执行下面几个步骤:

  1. 如果发现b、n已经被删除了,则执行对应的删除清理逻辑;

  2. 否则,如果没有找到待删除的(k, v),返回null;

  3. 如果找到了待删除的元素,也就是节点n,则把n的value置为null,同时在n的后面加上Marker节点,同时检查是否需要降低Index的层次。

  4. get分析

在这里插入图片描述

private V doGet(Object key) {
  Index<K,V> q;
  VarHandle.acquireFence();
  if (key == null)
    throw new NullPointerException();
  Comparator<? super K> cmp = comparator;
  V result = null;
  if ((q = head) != null) {
    outer: for (Index<K,V> r, d;;) {
      while ((r = q.right) != null) {
        Node<K,V> p; K k; V v; int c;
        if ((p = r.node) == null || (k = p.key) == null ||
         (v = p.val) == null)
          RIGHT.compareAndSet(q, r, r.right);
        else if ((c = cpr(cmp, key, k)) > 0)
          q = r;
        else if (c == 0) {
          result = v;
          break outer;
       }
        else
          break;
     }
      if ((d = q.down) != null)
        q = d;
      else {
        Node<K,V> b, n;
        if ((b = q.node) != null) {
          while ((n = b.next) != null) {
            V v; int c;
            K k = n.key;
            if ((v = n.val) == null || k == null ||
             (c = cpr(cmp, key, k)) > 0)
              b = n;
            else {
              if (c == 0)
                result = v;
              break;
           }
         }
       }
        break;
     }
   }
 }
  return result;
}  

无论是插入、删除,还是查找,都有相似的逻辑,都需要先定位到元素位置[b,n],然后判断b、n是否已经被删除,如果是,则需要执行相应的删除清理逻辑。这也正是无锁链表复杂的地方。

二、ConcurrentSkipListSet

如下面代码所示,ConcurrentSkipListSet只是对ConcurrentSkipListMap的简单封装,此处不再进一步展开叙述。

public class ConcurrentSkipListSet<E>
  extends AbstractSet<E>
  implements NavigableSet<E>, Cloneable, java.io.Serializable {
  // 封装了一个ConcurrentSkipListMap
  private final ConcurrentNavigableMap<E,Object> m;
 
  public ConcurrentSkipListSet() {
    m = new ConcurrentSkipListMap<E,Object>();
 }
 
  public boolean add(E e) {
    return m.putIfAbsent(e, Boolean.TRUE) == null;
 }
  // ...
}
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

穿城大饼

你的鼓励将是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值