JAVA并发包(十八):ConcurrentSkipListMap

ConcurrentSkipListMap内部是一种新的数据结构,可以叫它是跳跃链表结构,其节点相当于是excel中的表格,用了存储空间换取操作时间的,其查询时间复杂度是log(n)。另外插入时已做好节点的排序,遍历的时候就是排好序的了。

ConcurrentSkipListMap是可以并发操作的map,其key和value不能为null。其插入操作会比较耗性能,可以用在并发环境,插入少查询多的场景。

下面是它的结构示意图:

一、基本代码结构

从下面代码中看到,链表中新增加了一个Index对象,用来封装Node对象,其向下向右指向其他Index对象,构建起了跳跃链表。另外其实现了ConcurrentNavigableMap接口,这里不对这个导航maq做介绍了。还有Maq接口是没有继承Iterable接口,所有也不用实现迭代器方法。

public class ConcurrentSkipListMap<K,V> extends AbstractMap<K,V>
    implements ConcurrentNavigableMap<K,V>, Cloneable, Serializable {
    // 基础头节点对象,用来做初始化
	private static final Object BASE_HEADER = new Object();

    /** 头节点 */
    private transient volatile HeadIndex<K,V> head;

    /** 比较器 */
    final Comparator<? super K> comparator;

	/** 基础节点,保存原始数据 */
	static final class Node<K,V> {
        final K key;
        volatile Object value;
        volatile Node<K,V> next;
	}

	/** 跳跃链表的节点,封装node节点 */
	static class Index<K,V> {
        final Node<K,V> node;
        final Index<K,V> down;
        volatile Index<K,V> right;
	}

	/** 无比较器的构造方法 */
	public ConcurrentSkipListMap() {
        this.comparator = null;
        initialize();
    }

	/** 有比较器的构造方法 */
	public ConcurrentSkipListMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
        initialize();
    }

	/** 初始化方法,head节点的level为1 */
	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);
    }
}

二、put方法

put方法的实现在doPut方法里面,分3块代码构建纵向的index,可以理解是构建上图中值为119这个节点,下面结合代码分析。

	public V put(K key, V 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;
        // 第一步:构建底层的Node对象
        outer: for (;;) {
        	/** findPredecessor方法可以找到底层node节点的插入位置,在其后面插入新节点。
        	node链表简单的一维链表,把所有的对象连接起来,只是在上层用了Index对象做了跳跃链表 */
            for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
                if (n != null) {
                    Object v; int c;
                    Node<K,V> f = n.next;
                    // 读不一致,可能链表结构被改变了,重新遍历
                    if (n != b.next)               // inconsistent read
                        break;
                    // value为空,说明节点被清除了
                    if ((v = n.value) == null) {   // n is deleted
                    	/** 帮助清除节点,后面有该方法的解释。
                    	可能出现这样一种情况,前面线程做好了清除的标记,但是线程调度原因被挂起了,
                    	我们在当前线程调用helpDelete就可以加速清除被删除的节点。 */
                        n.helpDelete(b, f);
                        break;
                    }

					// value为空说明被清除了,value等于自己说明被标记清除,则退出内循环,重新遍历
                    if (b.value == null || v == n) // b is deleted
                        break;

					// 再次检查插入位置是否正确,如果大于0则向后找插入位置
                    if ((c = cpr(cmp, key, n.key)) > 0) {
                        b = n;
                        n = f;
                        continue;
                    }
			
					// 等于0说明链表中有key相等的节点,此时判断是否需要重新设置新的值
                    if (c == 0) {        
                        if (onlyIfAbsent || n.casValue(v, value)) {
                            @SuppressWarnings("unchecked") V vv = (V)v;
                            return vv;
                        }
                        // cas失败,则重新遍历
                        break; // restart if lost race to replace value
                    }
                    // else c < 0; fall through
                }
				//  创建新的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
            // 第二步:构建整个纵向的Index,其只做好了向下的指向
		    int level = 1, max;
		    // 构建跳跃表的level也是随机产生
            while (((rnd >>>= 1) & 1) != 0)
                ++level;
            Index<K,V> idx = null;
            HeadIndex<K,V> h = head;
            // level小于等于最大的level,则只做简单的纵向构建
            if (level <= (max = h.level)) {
                for (int i = 1; i <= level; ++i)
                    idx = new Index<K,V>(z, idx, null);
            }
			// 如果level大于最大的max,则需要新增一级level,因为level是由int值的位数决定的,所以其最大level为32
            else { // try to grow by one level
                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];
                // 遍历左右纵向的index构建,只做了向下的指向
                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<K,V> newh = h;
                    Node<K,V> oldbase = h.node;
                    // 头节点向上增加一级level,其右指向为新增的节点,可以参考结构图的第一行节点图
                    for (int j = oldLevel+1; j <= level; ++j)
                        newh = new HeadIndex<K,V>(oldbase, newh, idxs[j], j);
                    // 设置新的头节点
                    if (casHead(h, newh)) {
                        h = newh;
                        // 后面构建向右指向从老的level开始
                        idx = idxs[level = oldLevel];
                        break;
                    }
                }
            }

            // 第三步:设置整个纵向Index的向右指向,把跳跃链表连接起来
            // 外循环遍历每一级的index链
            splice: for (int insertionLevel = level;;) {
                int j = h.level;
                // 内循环找到当前level层的插入点,重新构建向右指向
                for (Index<K,V> q = h, r = q.right, t = idx;;) {
                    if (q == null || t == null)
                        break splice;
                    if (r != null) {
                        Node<K,V> n = r.node;
                        // compare before deletion check avoids needing recheck
                        int c = cpr(cmp, key, n.key);
                        // 如果发现value为null,则端口这个index节点
                        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) {
                    	// link方法就是重新构建向右指向的方法,后面有介绍
                        if (!q.link(r, t))
                            break; // restart
                        if (t.node.value == null) {
                            findNode(key);
                            break splice;
                        }
                        if (--insertionLevel == 0)
                            break splice;
                    }

                    if (--j >= insertionLevel && j < level)
                        t = t.down;
                    q = q.down;
                    r = q.right;
                }
            }
        }
        return null;
    }
		/** 该方法需要调用两次才会把清除的节点从链表中断开,第一次做标记清除,第二次做断开,说是可以减少CAS操作,其实我也没很懂 */
		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);
            }
        }

		final boolean link(Index<K,V> succ, Index<K,V> newSucc) {
            Node<K,V> n = node;
            // 新节点的右节点是老的右节点
            newSucc.right = succ;
            // 前驱的右节点为新节点
            return n.value != null && casRight(succ, newSucc);
        }

三、get方法

如果把以上put方法理解了,我们再来学习get方法就容易多了。简单来说get方法就是找到前驱节点,然后对比前驱的后继节点的key与给定的key是否一致。

	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;

				// 等于0,说明找到了key对应的节点,然后返回value
                if ((c = cpr(cmp, key, n.key)) == 0) {
                    @SuppressWarnings("unchecked") V vv = (V)v;
                    return vv;
                }
                // 小于0说明没有key对应的节点
                if (c < 0)
                    break outer;
                b = n;
                n = f;
            }
        }
        return null;
    }

四、remove方法

remove方法保证会把node的value值设置为null,然后尝试断开node和index链表。

	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;
                // 确保比较为0才会往下走
                if ((c = cpr(cmp, key, n.key)) < 0)
                    break outer;
                if (c > 0) {
                    b = n;
                    n = f;
                    continue;
                }
                // 进入这里说明匹配到了key
                // remove方法传入的value为null,故不会进入该分支
                if (value != null && !value.equals(v))
                    break outer;
                // 把value设置为null
                if (!n.casValue(v, null))
                    break;
                // 如果断开node节点失败的话,会调用findNode方法,其会找到value为null的节点,并且尝试把它从链表中断开。
                if (!n.appendMarker(f) || !b.casNext(n, f))
                    findNode(key);                  // retry via findNode
                else {
                	// findPredecessor方法里面可以断开value为null的index节点
                    findPredecessor(key, cmp);      // clean index
                    // 头节点的右边节点为null,则尝试减少level
                    if (head.right == null)
                        tryReduceLevel();
                }
                @SuppressWarnings("unchecked") V vv = (V)v;
                return vv;
            }
        }
        return null;
    }

	private void tryReduceLevel() {
        HeadIndex<K,V> h = head;
        HeadIndex<K,V> d;
        HeadIndex<K,V> e;
        // 大于3级才需要考虑较少level
        if (h.level > 3 &&
            (d = (HeadIndex<K,V>)h.down) != null &&
            (e = (HeadIndex<K,V>)d.down) != null &&
            e.right == null &&
            d.right == null &&
            h.right == null &&
            casHead(h, d) && // try to set
            h.right != null) // recheck 重新检查
            casHead(d, h);   // try to backout 回退
    }

五、其他方法

1.size方法
从以下代码可以看到size方法没有加锁,它是从节点的第一个元素遍历,统计value不为null的节点数。所以该方法不是原子安全的

	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.helpDelete(b, n.next);
        }
    }
  1. 判空方法
    判断map是否为空,直接检查第一个节点是否为null即可
	public boolean isEmpty() {
        return findFirst() == null;
    }
  1. 遍历方法
    遍历我们直接看entrySet() 方法即可,其迭代器的实现在EntryIterator类,EntryIterator的父类设置next的时候调用了findFirst()方法。迭代器每次调用完next() 方法时会调用advance(),重新设置next的值。从这些代码我们知道,ConcurrentSkipListMap的遍历是有序的,但是不保证原子和安全性。
	final class EntryIterator extends Iter<Map.Entry<K,V>> {
        public Map.Entry<K,V> next() {
            Node<K,V> n = next;
            V v = nextValue;
            advance();
            return new AbstractMap.SimpleImmutableEntry<K,V>(n.key, v);
        }
    }

	abstract class Iter<T> implements Iterator<T> {
        /** the last node returned by next() */
        Node<K,V> lastReturned;
        /** the next node to return from next(); */
        Node<K,V> next;
        /** Cache of next value field to maintain weak consistency */
        V nextValue;

        /** Initializes ascending iterator for entire range. */
        Iter() {
            while ((next = findFirst()) != null) {
                Object x = next.value;
                if (x != null && x != next) {
                    @SuppressWarnings("unchecked") V vv = (V)x;
                    nextValue = vv;
                    break;
                }
            }
        }

        public final boolean hasNext() {
            return next != null;
        }

        /** Advances next to higher entry. */
        final void advance() {
            if (next == null)
                throw new NoSuchElementException();
            lastReturned = next;
            while ((next = next.next) != null) {
                Object x = next.value;
                if (x != null && x != next) {
                    @SuppressWarnings("unchecked") V vv = (V)x;
                    nextValue = vv;
                    break;
                }
            }
        }
	}
  1. clear方法
    从下面方法中可以看到,clear没有加锁,不是原子性的。
public void clear() {
        for (;;) {
            Node<K,V> b, n;
            HeadIndex<K,V> h = head, d = (HeadIndex<K,V>)h.down;
            if (d != null)
            	// 首先把头节点降低到最低一级
                casHead(h, d);            // remove levels
            else if ((b = h.node) != null && (n = b.next) != null) {
                // 从最低一级的index链表逐个地把node的value节点设置为null,最后断开null为空的node节点
                Node<K,V> f = n.next;     // remove values
                if (n == b.next) {
                    Object v = n.value;
                    if (v == null)
                        n.helpDelete(b, f);
                    else if (n.casValue(v, null) && n.appendMarker(f))
                        b.casNext(n, f);
                }
            }
            else
                break;
        }
    }

六、总结

从上面的代码分析中,我们可以总结出ConcurrentSkipListMap的特点:

  1. 以存储空间换取查询和排序的时间,是一种便宜的查找和排序算法。
  2. CAS自旋实现插入操作。
  3. size方法,判断方法,遍历方法等不是原子安全的。
  4. key和value不予许为空。
  5. 在并发环境,少插入,多查找,并希望遍历有序的场景下可考虑使用。

参考:基于跳跃表的 ConcurrentSkipListMap 内部实现(Java 8)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值