JUC探险-14、ConcurrentSkipListMap

18 篇文章 0 订阅

一、:ConcurrentSkipListMap简介

  在Java世界里有多种实现key-value的数据结构,其各自都有着优缺点和适用场景。
    ●HashMap:插入、查找最快,为O(1);如使用链表实现则可实现无锁;数据有序化需要显式的排序操作。
    ●TreeMap:插入、查找为O(logn),但常数项较小;无锁实现的复杂性很高,一般需要加锁;数据天然有序。

  这里的Skip List也是一种key-value的数据结构。Skip List有着不低于红黑树的效率,但是其原理和实现的复杂度要比红黑树简单的多。

  Skip List简介

    Skip List,可以直译为跳表,它是一种可以替代平衡树的数据结构,其数据元素默认按照key值升序,天然有序。SkipList让已排序的数据分布在多层链表中,以0-1随机数决定一个数据的向上攀升与否,通过“空间来换取时间”的方式,在每个节点中增加了向前的指针,在插入、删除、查找时可以忽略一些不可能涉及到的结点,从而提高了效率。

    Skip List特性:
      ●由多层结构组成,level是通过一定的概率随机产生的。
      ●每一层都是一个有序的链表,默认是升序,也可以根据创建映射时所提供的Comparator进行排序,具体取决于使用的构造方法。
      ●最底层的链表包含所有元素。
      ●如果一个元素出现在Level i 的链表中,则它在Level i 之下的链表也都会出现。
      ●每个节点包含两个指针:一个指向同一链表中的下一个元素,一个指向下面一层的元素。


二、:关键属性及类

  首先来看注释中提供的结构示意:

* Head nodes          Index nodes
* +-+    right        +-+                      +-+
* |2|---------------->| |--------------------->| |->null
* +-+                 +-+                      +-+
*  | down              |                        |
*  v                   v                        v
* +-+            +-+  +-+       +-+            +-+       +-+
* |1|----------->| |->| |------>| |----------->| |------>| |->null
* +-+            +-+  +-+       +-+            +-+       +-+
*  v              |    |         |              |         |
* Nodes  next     v    v         v              v         v
* +-+  +-+  +-+  +-+  +-+  +-+  +-+  +-+  +-+  +-+  +-+  +-+
* | |->|A|->|B|->|C|->|D|->|E|->|F|->|G|->|H|->|I|->|J|->|K|->null
* +-+  +-+  +-+  +-+  +-+  +-+  +-+  +-+  +-+  +-+  +-+  +-+

  为了实现SkipList,ConcurrentSkipListMap提供了三个内部类来构建这样的链表结构:Node、Index、HeadIndex。其中Node表示最底层的单链表有序节点,Index表示为基于Node的索引层,HeadIndex用来维护索引level。
  也就是说,ConcurrentSkipListMap是通过HeadIndex维护索引level,通过Index从最上层开始往下层查找,一步一步缩小查询范围,最后到达最底层Node时,就只需要比较很小一部分数据了。

static final class HeadIndex<K,V> extends Index<K,V> {
	// 索引层,从1开始,Node单链表层为0
    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;
    }
}

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

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

  由源码可以看出:
    ●HeadIndex内部用一个level来定义层级。
    ●Index提供了一个基于Node节点的索引node,一个指向下一个Index的right节点,一个指向下层Index的down节点。
    ●Node的结构和一般的单链表毫无区别,key-value和一个指向下一个节点的next。


三、:重点方法分析

  ①构造方法

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);
}

    1、initialize()方法

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

      initialize()方法不仅仅只在构造函数中被调用,如clone(),clear()、readObject()时都会调用该方法进行初始化步骤。

  ②put()方法

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

private V doPut(K key, V value, boolean onlyIfAbsent) {
    Node<K,V> z;             // added node
    // key不能为空
    if (key == null)
        throw new NullPointerException();
    Comparator<? super K> cmp = comparator;
    // 在最底层插入节点
    outer: for (;;) {
    	// 定位key要插入的位置前一个node,b
        for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
        	// b的next不为空
            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;
        }
    }

	// 新建索引
    int rnd = ThreadLocalRandom.nextSecondarySeed();
    if ((rnd & 0x80000001) == 0) { // test highest and lowest bits
        int level = 1, max;
        // 抛硬币决定level(如果硬币为正面则level + 1,否则停止)
        while (((rnd >>>= 1) & 1) != 0)
            ++level;
        Index<K,V> idx = null;
        HeadIndex<K,V> h = head;
        // 如果决定的level比最高head.level小,直接生成index
        if (level <= (max = h.level)) {
        	// 由于需要确认每一level的down,所以需要从最下依次往上生成
            for (int i = 1; i <= level; ++i)
                idx = new Index<K,V>(z, idx, null);
        }
        // 如果决定的level比最高head.level大,则需要新增一个level
        else { // try to grow by one level
            level = max + 1; // hold in array and later pick the one to use
            // 初始化level + 1个item节点
            @SuppressWarnings("unchecked")Index<K,V>[] idxs =
                (Index<K,V>[])new Index<?,?>[level+1];
            for (int i = 1; i <= level; ++i)
                idxs[i] = idx = new Index<K,V>(z, idx, null);
            for (;;) {
                h = head;
                int oldLevel = h.level;
                // 由于并发,level已经被扩大了,需要重新开始
                if (level <= oldLevel) // lost race to add level
                    break;
                // 新的头结点HeadIndex
                HeadIndex<K,V> newh = h;
                Node<K,V> oldbase = h.node;
                // 生成新的HeadIndex节点,该HeadIndex指向新增level
                for (int j = oldLevel+1; j <= level; ++j)
                    newh = new HeadIndex<K,V>(oldbase, newh, idxs[j], j);
                // HeadIndex CAS替换
                if (casHead(h, newh)) {
                    h = newh;
                    idx = idxs[level = oldLevel];
                    break;
                }
            }
        }

		// 将index插入到相对应的level当中
        // find insertion points and splice in
        // 从插入的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);
                    // n.value == null,解除关系,r右移
                    if (n.value == null) {
                        if (!q.unlink(r))
                            break;
                        r = q.right;
                        continue;
                    }
                    // key > n.key,r右移
                    if (c > 0) {
                        q = r;
                        r = r.right;
                        continue;
                    }
                }

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

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

    代码比较长,梳理主要流程如下:
      ●首先可以看到,ConcurrentSkipList是不支持key或者value为null的。
      ●接着在最底层插入节点。
        通过findPredecessor()方法找到key要插入的位置前一个node,进行必要校验,将key-value包装成一个Node,然后通过CAS操作加入到最底层链表当中。
      ●新建索引。
        采取抛硬币方式决定level,如果所决定的level大于现存的最大level,则新增一个level。
      ●将index插入到相对应的层当中。
        新建一个Item链表,将其插入到SkipList结构中。

    1、findPredecessor()方法

      此方法主要功能是确认key要插入的位置。

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,表示该节点已经被删除了
                if (n.value == null) {
                	// 删掉r节点
                    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;
        }
    }
}

      代码主要流程如下:
        ●从最高层的headIndex开始向右一步一步比较,直到right为null或者右边节点的Node的key大于当前key为止。
        ●向下寻找,依次重复上一个过程,直到down为null为止。
      注意返回的结果是Node,不是Item,所以插入的位置应该是最底层的Node链表。

  ③get()方法

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 (;;) {
        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;
            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;
}

    get()方法要简单很多,实现跟put()方法前一部分类似,代码主要流程如下:
      ●首先通过findPredecessor()方法找到key要获取的位置前一个node。
      ●顺着right一直往右找,同时在这个过程中删除value为null的节点。

  ④remove()方法

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

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)
                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;
            }
            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;
}

    可以看到doRemove()方法有两个参数,从调用可以看出,方法既支持根据key进行remove,也支持匹配key-value进行remove。

    代码主要流程如下:
      ●首先通过findPredecessor()方法找到key要删除的位置前一个node。
      ●然后通过右移、比较,找到目标后利用CAS把value替换为null。
      ●最后判断该节点是不是这层唯一的index,如果是的话,调用tryReduceLevel()方法把这层干掉,完成删除。

    从代码中可以看出,remove方法仅仅是把Node的value设置null,并没有真正删除该节点。回顾上面的put()、get()方法,它们在寻找节点的时候都会判断节点的value是否为null,如果为null,则调用unLink()方法取消关联关系。

  ⑤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;
}

    ConcurrentSkipListMap的size()操作和ConcurrentHashMap不同,它并没有维护一个全局变量来统计元素的个数,所以每次调用该方法的时候都需要去遍历。

    代码主要流程如下:
      ●调用findFirst()方法找到第一个Node,然后利用node的next去统计。
      ●返回统计数据,最多能返回Integer.MAX_VALUE。


四、:总结

  在某些或者说大多数情况下,ConcurrentHashMap的存取速度比ConcurrentSkipListMap优越。但ConcurrentSkipListMap也有一些ConcurrentHashMap不能比拟的优点:
    ●ConcurrentSkipListMap的key是有序的。
    ●ConcurrentSkipListMap可以支持更高的并发。ConcurrentSkipListMap的存取时间是log(N),和线程数几乎无关。也就是说在数据量一定的情况下,并发的线程越多,ConcurrentSkipListMap越能体现出优势。
  注意,调用ConcurrentSkipListMap的size()时,由于多个线程可以同时对映射表进行操作,所以映射表需要遍历整个链表才能返回元素个数,这个操作的时间复杂度是O(log(n))。

系列文章传送门:

JUC探险-1、初识概貌
JUC探险-2、synchronized
JUC探险-3、volatile
JUC探险-4、final
JUC探险-5、原子类
JUC探险-6、Lock & AQS
JUC探险-7、ReentrantLock
JUC探险-8、ReentrantReadWriteLock
JUC探险-9、Condition
JUC探险-10、常见工具、数据结构
JUC探险-11、ConcurrentHashMap
JUC探险-12、CopyOnWriteArrayList
JUC探险-13、ConcurrentLinkedQueue
JUC探险-14、ConcurrentSkipListMap
JUC探险-15、BlockingQueue
JUC探险-16、ThreadLocal
JUC探险-17、线程池

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值