Java 并发 --- ConcurrentSkipListMap源码分析

在学习ConcurrentSkipListMap 之前 我们需要先来学习一种随机化的数据结构–跳跃表(skip list)
对于数组的查找可以有很多方法,如果是有序的,那么可以采用二分查找,二分查找要求元素可以随机访问,所以决定了需要把元素存储在连续内存。这样查找确实很快,但是插入和删除元素的时候,为了保证元素的有序性,就需要大量的移动元素了。 对于链表而言,不能进行随机访问,也就是说不能单纯的使用二分查找,只能通过顺序遍历的方式,为了使链表也能有很好的查询性能,因此才衍生出跳跃表这种数据结构,而且跳跃表相比平衡树,其实现也相对简单许多。

跳跃表介绍

跳跃表(skiplist)是一种随机化的数据结构, 跳跃表以有序的方式在层次化的链表中保存元素, 效率和平衡树媲美 —— 查找、删除、添加等操作都可以在对数期望时间下完成, 并且比起平衡树来说, 跳跃表的实现要简单直观得多,通过下面图示,可以很直观的了解跳跃表。

这里写图片描述

上面这张图就是一个跳跃表的实例,先说一下跳跃表的构造特征:

  • 一个跳跃表应该有若干个层(Level)链表组成;
  • 跳跃表中最底层的链表包含所有数据,上层链表存储的数据的引用;
  • 跳跃表中的数据是有序的;
  • 头指针(head)指向最高一层的第一个元素;

通过这样建立多层链表的方式,从顶层开始查找数据,实现了跳跃式的查询数据,利用空间来换取时间,下面就一起来看看跳跃表的建立以及查询过程。

跳跃表的查找

SkipListd的查找算法较为简单,对于上面我们我们要查找元素42,其过程如下:

  1. 比6大,往后找(20),20后面没有数据了,进入20的下一层
  2. 比20大,继续往后找,找到42.

这里写图片描述

跳跃表的插入

为什么说跳跃表是一种随机化的数据结构呢,那是因为在每次添加数据后,会随机决定这个数据是否能够攀升到高层次的链表中,也就是同样的数据构造出来的跳跃表可能是不相同的,这个过程我们看看下面的图解:
这里写图片描述

添加数据

添加数据42:
首先会先查找42应该插入位置的前驱节点,从head 所指节点开始查找(先向右,再向左),当查找节点20时,发现后面没有结点了,那么进行下一层进行查找,那么这样就会查找到节点42的前驱应该是30这个结点,然后将数据插入到最底层链表中:
这里写图片描述
然后通过随机过程确定节点42是否会像上进行攀爬,并且确定攀爬的层次,假设这里需要向上攀爬两层,那么其结果如下所示:
这里写图片描述

连接索引层

当然这样并没有完,现在我们把数据插入到了链表中,同时生成了索引层,那么还有一步,就是连接索引层
这里写图片描述

跳跃表的删除

跳跃表的删除其实和插入差不多,先查找节点,然后移除每一层的链接即可。
这里写图片描述

至此对跳跃表应该有了一定的认识了,下面来看看jdk 中ConcurrentSkipListMap 是如何实现跳跃表的,结合具体的分析,也可以加强理解。

ConcurrentSkipListMap(jdk 1.8)

ConcurrentSkipListMap 一个并发安全, 基于 skip list 实现有序存储的Map,ConcurrentSkipListMap提供了三个内部类来构建这样的链表结构:Node、Index、HeadIndex。其中Node表示最底层的单链表有序节点、Index表示为基于Node的索引层,HeadIndex用来维护索引层次。

继承体系

这里写图片描述

数据结构

Node节点定义:

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

    /**
     * Creates a new regular node.
     */
    Node(K key, Object value, Node<K,V> next) {
        this.key = key;
        this.value = value;
        this.next = next;
    }
    ... //省略部分方法
}

Index 定义:

static class Index<K,V> {
      final Node<K,V> node;  // 数据节点 引用
      final Index<K,V> down; //下层 Index
      volatile Index<K,V> right; // 右边Index

      /**
       * Creates index node with given values.
       */
      Index(Node<K,V> node, Index<K,V> down, Index<K,V> right) {
          this.node = node;
          this.down = down;
          this.right = right;
      }
      ...//省略部分代码
}

HeadIndex 定义:

/**
 * Nodes heading each level keep track of their level.
 */
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;
    }
}

HeadIndex 增加一个 level 属性用来标示索引层级; 注意所有的 HeadIndex 都指向同一个 Base_header 节点;

 /**
  * Special value used to identify base-level header
  */
 private static final Object BASE_HEADER = new Object();

 /**
  * The topmost head index of the skiplist.
  */
 private transient volatile HeadIndex<K,V> head; // HeadIndex的头指针

 /**
  * The comparator used to maintain order in this map, or null if
  * using natural ordering.  (Non-private to simplify access in
  * nested classes.)
  * @serial
  */
 final Comparator<? super K> comparator;  //元素比较器

上面是ConcurrentSkipListMap 中跳跃表的定义,但是仍然比较抽象,还是转换成比较好理解的图示吧:
这里写图片描述

注意:在上面的跳跃表的简单图示中,没有画出HeadIndex结构

构造方法

1、默认构造

/**
 * Constructs a new, empty map, sorted according to the
 * {@linkplain Comparable natural ordering} of the keys.
 */
public ConcurrentSkipListMap() {
    this.comparator = null;
    initialize();
}

2、指定比较器

/**
 * Constructs a new, empty map, sorted according to the specified
 * comparator.
 */
public ConcurrentSkipListMap(Comparator<? super K> comparator) {
    this.comparator = comparator;
    initialize();
}

3、通过集合初始化

/**
 * Constructs a new map containing the same mappings as the given map,
 * sorted according to the {@linkplain Comparable natural ordering} of
 * the keys.
 */
public ConcurrentSkipListMap(Map<? extends K, ? extends V> m) {
    this.comparator = null;
    initialize();
    putAll(m);
}

/**
 * Constructs a new map containing the same mappings and using the
 * same ordering as the specified sorted map.
 */
public ConcurrentSkipListMap(SortedMap<K, ? extends V> m) {
    this.comparator = m.comparator();
    initialize();
    buildFromSorted(m);
}

构造方法中都调用了initialize() 方法

private void initialize() {
    keySet = null;
    entrySet = null;
    values = null;
    descendingMap = null;
    //初始化head
    head = new HeadIndex<K,V>(new Node<K,V>(null, BASE_HEADER, null),
                              null, null, 1);
}

初始创建的跳跃表结构如下:
这里写图片描述

添加数据(put)

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

要求value 不能为空,内部调用doPut 方法。

findPredecessor 方法

findPredecessor 方法的功能是寻找某个key的前驱(如果遇到需要删除的节点,那么进行辅助删除)。从最高层的headIndex开始向右一步一步比较,直到right为null或者右边节点的Node的key大于当前key为止,然后再向下寻找,依次重复该过程,直到down为null为止,即找到了前驱。

private Node<K,V> findPredecessor(Object key, Comparator<? super K> cmp) {
    if (key == null)
        throw new NullPointerException(); // don't postpone errors
    for (;;) {
        // 从head 开始遍历
        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) {
                    if (!q.unlink(r))
                        break;           // restart
                    r = q.right;         // reread r
                    continue;
                }
                 // 如果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;
        }
    }
}
doPut 方法
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 (;;) {
        // b 为前继节点, n是前继节点的next
        for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
            if (n != null) {
                //b 是链表的最后一个节点
                Object v; int c;
                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;
                }
                if (b.value == null || v == n) // b is deleted
                    break;
                 // 节点大于,往后继续查找  
                if ((c = cpr(cmp, key, n.key)) > 0) {
                    b = n;
                    n = f;
                    continue;
                }
                //相等,根据参数onlyIfAbsent  决定是否覆盖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
            }
            //创建节点
            z = new Node<K,V>(key, value, n);
            //插入节点,如果失败,则重来
            if (!b.casNext(n, z))
                break;         // restart if lost race to append to b
            break outer;
        }
    }
    //随机数
    int rnd = ThreadLocalRandom.nextSecondarySeed();
    // 判断是否需要添加level
    if ((rnd & 0x80000001) == 0) { // test highest and lowest bits
        int level = 1, max;
        //获取 level
        while (((rnd >>>= 1) & 1) != 0)
            ++level;
        Index<K,V> idx = null;
        HeadIndex<K,V> h = head;
        //如果层次level大于最大的层次话则需要新增一层,否则就在相应层次以及小于该level的层次进行节点新增处理。
        // level比最高层次head.level小,直接生成需要的index
        if (level <= (max = h.level)) {
            for (int i = 1; i <= level; ++i) //生成index
                idx = new Index<K,V>(z, idx, null);
        }
        else { // level > max 
            level = max + 1; // hold in array and later pick the one to use
            @SuppressWarnings("unchecked")Index<K,V>[] idxs =
                (Index<K,V>[])new Index<?,?>[level+1];
            for (int i = 1; i <= level; ++i) //生成index
                idxs[i] = idx = new Index<K,V>(z, idx, null);
            for (;;) {
                h = head;
                int oldLevel = h.level;
                // 层次扩大了,需要重新开始(其它线程改变了跳跃表)
                if (level <= oldLevel) // lost race to add level
                    break;
                HeadIndex<K,V> newh = h;
                Node<K,V> oldbase = h.node;
                // 生成新的HeadIndex节点
                for (int j = oldLevel+1; j <= level; ++j)
                    newh = new HeadIndex<K,V>(oldbase, newh, idxs[j], j);
                //更新head    
                if (casHead(h, newh)) {
                    h = newh;
                    idx = idxs[level = oldLevel];
                    break;
                }
            }
        }
        /**
         *前面生成了索引层,但是并没有将这些Index插入到相应的层次当中
         *下面的代码就是将index链接到相对应的层当中
         */
        // 从插入的层次level开始
        splice: for (int insertionLevel = level;;) {
            int j = h.level;
             //从headIndex开始
            for (Index<K,V> q = h, r = q.right, t = idx;;) {
                if (q == null || t == null)
                    break splice;
                // r != null;这里是找到相应层次的插入节点位置,注意这里只横向找    
                if (r != null) {
                    Node<K,V> n = r.node;
                    // compare before deletion check avoids needing recheck
                    int c = cpr(cmp, key, n.key);
                    //需要删除r
                    if (n.value == null) {
                        if (!q.unlink(r))
                            break;
                        r = q.right; //继续向后
                        continue;
                    }
                    //向右进行遍历
                    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;
                    }
                    //链接完毕
                    if (--insertionLevel == 0)
                        break splice;
                }
                //向下继续链接其它index 层
                if (--j >= insertionLevel && j < level)
                    t = t.down;
                q = q.down;
                r = q.right;
            }
        }
    }
    return null;
}

doPut方法代码比较长,来梳理一下逻辑:
1、通过 findPredecessor()方法确认key要插入的位置
2、进行数据校验,如果发现需要删除节点,则进行辅助删除,如果其他线程改变了跳跃表,则进行重试或遍历查找合适的位置。
3、如果跳跃表中已经存在该key,则根据onlyIfAbsent 确定是否覆盖旧值。
4、生成节点,插入到最底层的数据链表中。
5、根据随机值确定是否创建索引层,如果不需要则返回,否则执行第6步
6、如果需要创建的索引层超过最大的level,则需要创建HeadIndex 索引层,否则只需要创建Index 索引层即可。
7、从head 开始进行遍历,将每一层的新添加的Index索引层进行连接(这个可以结合上面跳跃表连接索引层图示来理解)。

查找数据

public V get(Object key) {
    return doGet(key);
}
private V doGet(Object key) {
    if (key == null)
        throw new NullPointerException();
    Comparator<? super K> cmp = comparator;
    outer: for (;;) {
        // 通过findPredecessor 方法查找其前驱节点
        for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
            Object v; int c;
            if (n == null) //不存在该key
                break outer;
            Node<K,V> f = n.next;
            //其它线程更新了跳跃表,重试
            if (n != b.next)                // inconsistent read
                break;
            //需要删除数据    
            if ((v = n.value) == null) {    // n is deleted
                n.helpDelete(b, f); //协助删除
                break;
            }
            //b会被删除,重来
            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) //不存在该key
                break outer;
            // 其它线程添加了数据,向后继续遍历查找    
            b = n;
            n = f;
        }
    }
    return null;
}

查找数据比较简单,先通过findPredecessor 查找其前驱,然后顺着right一直往右找即可,同时在这个过程中,如果发现某个节点需要删除,则需要进行辅助删除,如果发现跳跃表数据结构被其它线程改变,会重新尝试获取其前驱。

删除数据

ConcurrentSkipListMap 支持并发操作,因此在删除的时候需要注意,因为在删除的同时,其它线程可能在该位置上进行数据插入,这样很容易造成数据的丢失,这个我们在前面的阻塞队列SynchronousQueue 也遇到类似的问题,在SynchronousQueue 中用了一个cleanMe标记需要删除的节点的前驱,在ConcurrentSkipListMap 中也是类似的机制,会在删除后面添加一个特殊的节点进行标记,然后再进行整体的删除,如果不进行标记,那么如果正在删除的节点,可能其它线程正在此节点后面添加数据,造成数据丢失.

public V remove(Object key) {
    return doRemove(key, null);
}

调用doRemove()方法,doRemove有两个参数,一个是key,另外一个是value,所以doRemove方法即提供remove key,也提供同时满足key-value。

//指定key 和value删除相应的节点
final V doRemove(Object key, Object value) {
    if (key == null)
        throw new NullPointerException();
    Comparator<? super K> cmp = comparator;
    outer: for (;;) {
        //查找其前驱
        for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
            Object v; int c;
            if (n == null) // 不存在该key
                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即将也被删除
            if (b.value == null || v == n)      // b is deleted
                break;
            // 不存在该key    
            if ((c = cpr(cmp, key, n.key)) < 0)
                break outer;
            if (c > 0) { //向后继续遍历查找
                b = n;
                n = f;
                continue;
            }
            //key 相等,value 不相等,退出
            if (value != null && !value.equals(v))
                break outer;
            //逻辑删除,设置value=null    
            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
                 //该层已经没有节点,删掉该层
                if (head.right == null)
                    tryReduceLevel();
            }
            @SuppressWarnings("unchecked") V vv = (V)v;
            return vv;
        }
    }
    return null;
}

调用findPredecessor()方法找到前驱节点,然后通过向右遍历,然后比较,找到后利用CAS把value替换为null(这样其它线程可以感知到这个节点状态,协助完成删除操作),添加删除标记节点,添加成功后,再执行删除操作(因为添加标记的cas和其它线程在这个节点后面添加数据的cas 只有一个能成功,所以可以避免数据丢失),如果该层已经没有了其它节点,调用tryReduceLevel()方法把这层移除掉。

添加标记节点:

boolean appendMarker(Node<K,V> f) {
    return casNext(f, new Node<K,V>(f));
}

协助删除节点:

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); //执行删除操作
    }
}

size统计

ConcurrentSkipListMap的size()操作和ConcurrentHashMap不同,它并没有维护一个全局变量来统计元素的个数,每次调用该方法的时候都需要去遍历,在并发环境下,该size值只是一个瞬间值。

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;
}
final Node<K,V> findFirst() {
    for (Node<K,V> b, n;;) {
        //没有数据
        if ((n = (b = head.node).next) == null)
            return null; 
        if (n.value != null)
            return n;
        //协助删除n这个节点    
        n.helpDelete(b, n.next);
    }
}

调用findFirst()方法找到第一个Node(如果有需要删除的节点,则进行协助删除),然后利用node的next去统计。最后返回统计数据,最多能返回Integer.MAX_VALUE

总结

ConcurrentSkipListMap 是一个基于 Skip List 实现的并发安全, 非阻塞读/写/删除 的 Map(key和value 不能为null),它的value是有序存储的, 其内部是由纵横链表组成, 通过空间换时间的方式,使得链表也可以实现类似的二分查找,相比平衡树,其编程复杂度大大减小.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值