一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第8天,点击查看活动详情。
基于JDK1.8详细介绍了ConcurrentSkipListMap的底层源码实现,包括跳跃表的原理,以及结点的插入、删除、查找、随机数算法、导航方法等底层源码!
1 跳跃表(SkipList)的概述
1.1 查找的概述
开发过程中,我们常常需要在一批数据集合中根据关键信息查询某些数据。
最常见的支持查询数据结构,比如普通的数组、链表等,他们的实现非常简单,但是查询效率却非常低下,根据关键字去查询的时间复杂度为O(n),这样的结果的主要原因是数据结构中的数据分布没有规律,我们只能重头到尾依次查找。
后来出现了一些性能比较好的数据结构,最开始的是二叉排序树,它将内部的数据根据指定的关键字进行排序,并构建成一棵二叉树的形状,通过比较关键字的大小不断的缩小查找范围,有效的提升了查找效率。
在理想情况下,二叉查找树的时间复杂度为O(logn),即类似二分查找,但是极端情况下,比如元素本来就是有序的,那此时二叉查找树退化为链表,查询某个元素同样需要O(n)的时间。
后来有了平衡二叉树(AVL树),它避免了二叉排序树的极端情况,让二叉树更加“平衡”,但是在由于“绝对平衡“的要求可能导致过多的旋转操作,甚至平衡的时间超过查找的时间。后来就有了红黑树,追求“相对平衡”,综合性能更好,但是增加了结点的颜色属性,实现难度更大。
另外还有其他的查找结构,比如哈希表,理想情况下哈希表查询时间复杂度为O(1),但是可能存在哈希冲突,导致性能降低,并且哈希函数的选择至今为止仍然是一个难题,哈希冲突的解决也比较麻烦。
上面的查找数据结构中,我们能看出来查询的综合效率越来越高,但是它们的实现的难度也越来越大(比如红黑树、性能优异的哈希表)。那么有没有一种实现起来比较简单,查询效率也比较快的数据结构呢?当然有,比如——跳跃表(SkipList)!
1.2 跳跃表的原理
跳跃表(SkipList),名字听起来挺高大上的,难道查询的而时候可以跳跃式的查找,实际上还真的可以;另外它的名字还有一个“表”,难道底层数据结构类似于链表?实际上还真的非常相似!我们上面说到跳跃表的结构比简单,那么我们猜测一下,它是由普通的链表和可以跳跃式查找的结构组成的,说白了就是拥有可以“直达”的辅助结构加持的普通链表!
实际上,这个类似的辅助结构我们能想到索引,比如数据库表如果加了索引,那么查找索引效率会快很多。这里的链表加上“索引”就变成了跳跃表。如下图所示:
最底层是一个排序了的链表,保存了具体的数据,然后往上是一层层的索引,这里的索引实际上也是一个个的结点,但是可以不保存数据而是保存最底层数据结点的引用,同时某一层级的索引结点保存右边结点和下层结点的引用。
当访问数据的时候,从最上层的索引头开始查找,找到了就直接通过引用访问最下层对应的结点然后返回了,找不到也没关系,因为这里的元素需要支持排序,这样就能找到范围,然后在对应范围起点查找下一级索引,直到最底部数据。这样就实现了链表数据的跳跃式查找。
理想情况下上一层结点数量是下一层的1/2,并且处于下层三个连续结点的中间,这样的话时间复杂度为O(logn)。比如上图完美的跳跃表的样式,我们查询任何一个数,最多只需要三次即可返回结果,比如我们查询6,首先在L2查询两次定位到5,然后向下在L1定位到5-7之间,最后在数据链表中直接定位到6,这样就将2-4位置的结点跳过了。
理想的结构确实不错,但是想要一直致维持这样的结构却非常的困难,因为随机的插入删除会将最开始的跳跃表结果完全打乱,为此如果每一次的添加、删除结点之后都要调整跳跃表的结构,那么它的实现和“简单”也沾不上边了,那么怎么办呢?
跳跃表和其他数据结构重要的区别之一就是跳跃表使用了随机数算法,类似于“抛硬币”。每次插入一个结点之后,采用随机数算法判断是否需要向上构建索引,如果不需要,那么就直接返回了;如果需要,那么直接在当前层的上一层添加一个对应索引结点,然后,继续采用随机数算法判断是否需要继续向上构建索引,如果需要,那么在上一层的基础上,在上上层添加一个对应索引结点……直到某一次添加之后随机数算法判断不需要再添加索引为止。而删除操作则非常的简单,直接删除对应的结点和与它相关的索引,并重新建立其他结点的引用关系即可。
这样的随机数算法,每一次添加到结点是否构建索引的概率都是二分之一,在数据量比较少的时候,可能构建的索引不是很均匀,但是在数据量非常多的情况下,通过概率论计算,最终的表结构非常接近于理想跳跃表,即时间复杂度就是O(logn);此时跳跃表的实现就非常简单的了,完全不需要什么“平衡”一个靠“运气”,并且SkipList支持排序。
上面的随机数算法仅仅是一种介绍,不同的跳表的实现有自己的随机数算法和索引层级增长规律,因此不必过于纠结。 在Reids的zset数据类型,底层就是以跳跃表为基础来实现的;LevelDB中也存在跳跃表的数据结构。另外在Java中也提供了跳跃表的实现:ConcurrentSkipListMap和ConcurrentSkipListSet。
2 ConcurrentSkipListMap的概述
public class ConcurrentSkipListMap<K,V> extends AbstractMap<K,V> implements ConcurrentNavigableMap<K,V>, Cloneable, Serializable
在开发过程中,我们经常需要对某一些键值对集合进行排序以及查找边界,我们常用的集合就是TreeMap,它采用红黑树实现,能够根据我们指定的关键字进行排序。
但是,TreeMap是线程不安全的集合,在多线程环境下无法使用。在JDK1.6的时候终于出现了支持排序的并且线程安全的集合:ConcurrentSkipListMap。
ConcurrentSkipListMap实现了ConcurrentNavigableMap接口,而ConcurrentNavigableMap又继承了NavigableMap接口,因此类似于TreeMap,支持一系列导航操作的方法,比如一系列的lower、floor、ceiling 和 higher开头的方法,分别返回与小于、小于等于、大于等于、大于给定参数的相关对象。同时,它还支持大量线程高速并发操作,即ConcurrentSkipListMap是线程安全且并发性能良好的排序Map。
因此,可以将ConcurrentSkipListMap看成并发版本的TreeMap。但是它们的底层实现却完全不同,TreeMap基于红黑树(Red-Black Tree)实现,而ConcurrentSkipListMap则是基于跳跃表(SkipList)实现。
跳跃表是另一种实现支持排序且性能较好的Map的思路,是一种以空间换时间的思想,通过添加额外的索引结构,进而提升查询的效率。另外ConcurrentSkipListMap也是基于一种CAS乐观锁的方式去实现线程安全和高并发。
为什么使用跳跃表来实现安全并发的排序Map而不采用红黑树来实现呢?在看了下面的跳跃表的实现就懂了。
ConcurrentSkipListMap还实现了Cloneable和Serializable接口,因此支持克隆和序列化!
ConcurrentSkipListMap不允许 null key 和 null value!
3 ConcurrentSkipListMap的实现
3.1 主要全局属性
主要全局属性比较简单,也比较少。
一个BASE_HEADER的Object对象,用来当作最底层数据链表的头结点的value值;一个head引用,用来保存最上层索引链表的头结点引用,通过它就可以遍历整张跳跃表;还有一个自定义比较器的引用comparator,如果它为null,那么就会使用自然顺序比较并排序。
/**
* 最底层数据链表的头结点的value值
*/
private static final Object BASE_HEADER = new Object();
/**
* 最上层索引链表的头结点引用,通过它可以遍历整张跳跃表
*/
private transient volatile HeadIndex<K, V> head;
/**
* 指定全局比较器,用于比较两个元素的关键字大小并进行排序
* 如果在构造器中没有显式传入指定比较器,则默认对key按照自然顺序排序
*/
final Comparator<? super K> comparator;
//一系懒加载的全局属性
/**
* 保存跳跃表的key的set集合的引用,在第一次调用keySet方法时才会初始化
*/
private transient KeySet<K> keySet;
/**
* 保存跳跃表的key-value结点的Set集合的引用,在第一次调用entrySet方法时才会初始化
*/
private transient EntrySet<K, V> entrySet;
/**
* 保存跳跃表的value的Collection集合的引用,在第一次调用values方法时才会初始化
*/
private transient Values<V> values;
/**
* 保存跳跃表的key-value结点的逆序排序的Map集合的引用,在第一次调用descendingMap方法时才会初始化
*/
private transient ConcurrentNavigableMap<K, V> descendingMap;
复制代码
3.2 主要内部类
ConcurrentSkipListMap为了实现跳跃表,内部具有三种不同类型的结点。
3.2.1 Node数据结点
Node内部类就是跳跃表的最底层的链表的类型,主要保存真正的数据,或者作为一个删除标记结点,以及和数据相关的操作。包括key-键、volatile 的value-值、volatile 的next-后继结点引用!
/**
* 最底层链表的数据结点
* 保存了真正的数据或者删除标记
*/
static final class Node<K, V> {
/**
* key
*/
final K key;
/**
* vallue,使用volatile修饰
*/
volatile Object value;
/**
* 结点后继引用,使用volatile修饰
*/
volatile Node<K, V> next;
/**
* 创建一个新的数据结点,用于存储数据
*
* @param key k
* @param value v
* @param next 后继
*/
Node(K key, Object value, Node<K, V> next) {
this.key = key;
this.value = value;
this.next = next;
}
/**
* 创建一个新的标记结点,用于标记前一个结点已被删除
*
* @param next 后继
*/
Node(Node<K, V> next) {
//key为null
this.key = null;
//value指向自己
this.value = this;
//保存next引用
this.next = next;
}
//还有一些基于当前数据结点的 CAS 方法,后面会讲到
}
复制代码
3.2.2 Index索引结点
Index内部类就是跳跃表的最底层的数据链表之上的索引链表的结点类型,主要用来保存索引关系,以及和索引相关操作。
包括一个对应的最底层链表的数据结点的引用node,一个对应的指向下一层索引链表的索引结点down,一个该索引链表的当前结点后继索引结点right,使用volatile修饰。
/**
* 索引结点,最底层的数据链表之上的索引链表的普通结点
*/
static class Index<K, V> {
/**
* 对应的最底层链表的数据结点
*/
final Node<K, V> node;
/**
* 对应的指向下一层索引链表的索引结点
*/
final Index<K, V> down;
/**
* 该索引链表的当前结点后继索引结点,使用volatile修饰
*/
volatile Index<K, V> right;
/**
* 创建一个新的索引结点
*
* @param node 对应的最底层链表的数据结点
* @param down 对应的指向下一层索引链表的索引结点
* @param right 该索引链表的当前结点后继索引结点
*/
Index(Node<K, V> node, Index<K, V> down, Index<K, V> right) {
this.node = node;
this.down = down;
this.right = right;
}
//还有一些基于当前索引结点的 CAS 方法,后面会讲到
}
复制代码
3.2.3 HeadIndex头索引结点
HeadIndex内部类就是跳跃表的最底层的数据链表之上的索引链表的头结点类型,主要用来保存索引链表的层级。
继承了Index内部类,除了具有node、down、right之外,额外新增了一个属性level,用于保存当前索引链表的层级。
/**
* 最底层的数据链表之上的索引链表的头索引结点,继承了索引结点Index
*/
static final class HeadIndex<K, V> extends Index<K, V> {
/**
* 保存当前索引链表的层级
*/
final int level;
/**
* 创建一个新的头索引结点
*
* @param node 对应的最底层链表的数据结点
* @param down 对应的指向下一层索引链表的索引结点
* @param right 该索引链表的当前结点后继索引结点
* @param level 当前索引链表的层级
*/
HeadIndex(Node<K, V> node, Index<K, V> down, Index<K, V> right, int level) {
super(node, down, right);
this.level = level;
}
}
复制代码
3.3 构造器
ConcurrentSkipListMap提供了4种构造器,会通过initialize进行跳表的初始化,默认初始化之后的跳表结构如下:
可以看到,默认初始化的跳表中,具有一个数据链表,内部只有一个头结点,value为BASE_HEADER,相当于一个哨兵结点;另外还具有一个索引链表,内部只有一个头索引结点,层级为1,head全局变量指向该头索引结点,该头索引结点又通过node属性指向底层数据链表头结点。这样就形成了最初始化的链表。
3.3.1 ConcurrentSkipListMap()
public ConcurrentSkipListMap()
构造一个新的空跳表,该跳表将按照键的自然顺序(Comparable. compareTo)进行排序。
/**
* 构造一个新的空跳表,该跳表将按照键的自然顺序进行排序。
*/
public ConcurrentSkipListMap() {
//指定比较器为null,按照键的自然顺序进行排序。
this.comparator = null;
//调用initialize方法初始化一系列属性、初始化跳表
initialize();
}
/**
* 初始化或者重置一系列的属性,主要比较器需要单独的初始化或者重置
*/
private void initialize() {
//懒加载的属性都置空
keySet = null;
entrySet = null;
values = null;
descendingMap = null;
//初始化一个数据链表的头结点,key和next为null,value则为BASE_HEADER
//初始化head属性,node指向数据链表头结点,down为null,right为null,level为1
head = new HeadIndex<K, V>(new Node<K, V>(null, BASE_HEADER, null),
null, null, 1);
}
复制代码
3.3.2 ConcurrentSkipListMap( comparator )
public ConcurrentSkipListMap(Comparator<? super K> comparator)
构造一个新的跳表,该跳表按照指定的比较器comparator进行排序。comparator是Comparator接口的实现,如果为null那么还是按照自然顺序进行排序。
/**
* 构造一个新的跳表,该跳表按照指定的比较器comparator进行排序。
*
* @param comparator 指定比较器,Comparator接口的实现,如果为null那么还是按照自然顺序进行排序
*/
public ConcurrentSkipListMap(Comparator<? super K> comparator) {
//comparator为指定比较器,这里没有null校验,如果为null那么还是按照自然顺序进行排序
this.comparator = comparator;
//调用initialize方法初始化一系列属性、初始化跳表
initialize();
}
复制代码
3.3.3 ConcurrentSkipListMap(Map m )
public ConcurrentSkipListMap(Map<? extends K,? extends V> m)
构造一个新跳表,该跳表所包含的映射关系与指定Map集合包含的映射关系相同,并按照键的自然顺序进行排序。
如果指定集合的key不是Comparable的实现或者无法进行比较,那么抛出ClassCastException;如果 m 或者 它的key 或者value 为null,那么抛出NullPointerException。
/**
* 构造一个新跳表,该跳表所包含的映射关系与指定Map集合包含的映射关系相同,并按照键的自然顺序进行排序。
*
* @param m 指定Map
* @throws ClassCastException 如果 m的key不是Comparable接口的实现 或者 无法进行比较
* @throws NullPointerException 如果 m 或者 它的key 或者value 为null
*/
public ConcurrentSkipListMap(Map<? extends K, ? extends V> m) {
//指定比较器为null,按照键的自然顺序进行排序。
this.comparator = null;
//调用initialize方法初始化一系列属性、初始化跳表
initialize();
//直接调用putAll方法
putAll(m);
}
复制代码
3.3.4 ConcurrentSkipListMap(SortedMap m)
public ConcurrentSkipListMap(SortedMap<K,? extends V> m)
构造一个新跳表,该跳表所包含的映射关系与指定的有序集合包含的映射关系相同,使用的排序顺序也相同。
如果 m 或者 它的key 或者value 为null,那么抛出NullPointerException。
/**
* 构造一个新跳表,该跳表所包含的映射关系与指定的有序集合包含的映射关系相同,使用的排序顺序也相同。
*
* @param m 指定的排序的Map
* @throws NullPointerException 如果 m 或者 它的key 或者value 为null
*/
public ConcurrentSkipListMap(SortedMap<K, ? extends V> m) {
//指定比较器为m的comparator()方法返回的比较器,即m集合的comparator属性值
this.comparator = m.comparator();
//调用initialize方法初始化一系列属性、初始化跳表
initialize();
//类似于putAll方法,但是由于m已经是排序的因此更加简化
buildFromSorted(m);
}
复制代码
3.4 put操作
public V put(K key, V value)
插入指定键值对。如果跳表以前包含了一个该键的映射关系,那么将新值替换旧值。返回以前与指定键关联的值;如果该键没有映射关系,则返回 null。
如果指定键无法与当前跳表中的键进行比较,那么抛出ClassCastException;如果指定键或者值为null,那么抛出NullPointerException。
put方法是ConcurrentSkipListMap的核心方法之一,源码比较多,且相对复杂。
/**
1. 插入指定键值对。如果跳表以前包含了一个该键的映射关系,那么将新值替换旧值。
2. 3. @param key 指定键
4. @param value 指定值
5. @return 返回以前与指定键关联的值;如果该键没有映射关系,则返回 null。
6. @throws ClassCastException 如果指定键无法与当前跳表中的键进行比较
7. @throws NullPointerException 如果指定键或者值为null
*/
public V put(K key, V value) {
//value检验
if (value == null)
throw new NullPointerException();
//调用内部的doPut方法,传递key、value、false
//doPut方法是插入数据的公用内部方法
return doPut(key, value, false);
}
复制代码
3.4.1 doPut插入结点
doPut方法是插入数据的公用内部方法,也是提供了插入逻辑的主要代码实现,它根据指定的模式onlyIfAbsent插入数据,这类似于ConcurrentHashMap的内部的putVal方法。computeIfAbsent、compute、merge、putIfAbsent等方法内部也调用了doPut方法。大概步骤为:
- 对key进行null校验;
- 开启一个死循环,尝试添加结点或者替换value,两者之一成功便退出循环。替换value成功之后doPut方法结束,返回旧value;添加新结点成功之后便进入下一步;在循环途中会帮助删除value为null的数据结点。
- 添加结点成功之后,那么开始采用随机数算法判断是否需要增加索引结点,如果不需要,那么doPut方法结束,返回旧value;如果需要,那么这里分两步:
- 通过随机数计算具体需要新增多少个索引结点,如果新增的索引结点超过目前最大索引层级,那么说明需要新增索引层级,然后构建新增索引结点之间的纵向引用关系(down、node)以及新增层级的新增索引结点的横向引用关系(right);
- 构建原索引层级的结点和新增结点之间的横向引用关系(right),到此对于新增结点成功构建了索引,doPut方法结束,返回null;
/**
* 插入逻辑的主要方法实现。 添加元素(如果不存在)或替换值(如果存在且 onlyIfAbsent 为 false)
*
* @param key k
* @param value v
* @param onlyIfAbsent 在JDK1.8的新方法putIfAbsent中传递true;put和putAll方法中传递false
* 如果为true,并且传入的key已经存在,那么不进行value替换,返回旧的value。如果不存在key,就添加key和value,返回null。
* 如果为false,并且传入的key已经存在,那么进行value替换,并返回旧的value。如果不存在key,就添加key和value,返回null;
* @return 旧值或者null(新插入)
*/
private V doPut(K key, V value, boolean onlyIfAbsent) {
//保存待添加的新结点
Node<K, V> z; // added node
//key检验
if (key == null)
throw new NullPointerException();
//cmp保存全局的比较器,可能为null
Comparator<? super K> cmp = comparator;
/*
* 1 第一步,开启一个死循环,尝试添加结点或者替换value,两者之一成功便退出循环
* 替换value成功之后便退出方法,添加结点成功之后便进入第二步
*/
outer:
for (; ; ) {
/*
* 调用findPredecessor查找小于指定key的具有索引关系的最大Node结点,没找到就返回数据链表头结点,返回值赋值为b;n=b.next,即后继
* 内部再开启一个死循环
*/
for (Node<K, V> b = findPredecessor(key, cmp), n = b.next; ; ) {
//如果n不为null,说明b的后继不为null
if (n != null) {
Object v;
int c;
//f=n.next,即f为n的后继
Node<K, V> f = n.next;
//如果此时n不等于b.next,因为ConcurrentSkipListMap没有锁,因此可能在这里的b的后继发生了改变,比如有另一个线程抢先一步在b后面插入了结点
if (n != b.next) // inconsistent read
//break跳出内层循环,继续外层循环,相当于重新开始内层循环,重新查找b、n
break;
//如果n的value为null,说明n结点的数据被删除了,但是后续引用关系没有删除
if ((v = n.value) == null) { // n is deleted
//n调用Node结点的helpDelete方法帮助删除数据结点
n.helpDelete(b, f);
//帮助成功之后,break跳出内层循环,继续外层循环,相当于重新开始内层循环,重新查找b、n
break;
}
/*
* 如果b的value为null,
* 或者 v等于n,即n是一个标记结点
* 说明b结点的数据被删除了,但是后续引用关系没有删除
*/
if (b.value == null || v == n) // b is deleted
//这里没法帮忙了
//break跳出内层循环,继续外层循环,相当于重新开始内层循环,重新查找b、n
break;
/*
* findPredecessor方法是查找小于指定key的具有索引关系的最大Node结点,但同时可能存在与key更加接近的但是没有索引关系的结点,
* 这里需要查找出来,这一步就是在数据链表中查找真正的小于或者等于key的最大结点
* 调用cpr将指定key和n的key相比较获取结果c,如果指定key大于n的key,说明还存在比b更大的小于指定key的结点
* 那么b = n,n = f
*/
if ((c = cpr(cmp, key, n.key)) > 0) {
//如果指定key大于n的key,那么b = n,n = f
b = n;
n = f;
//改变b、n的引用,都向后移动一位,然后continue结束本次内层循环,继续下一次循环,直到找到真正的小于等于key的最大结点
continue;
}
/*如果c==0,说明找到一个key相等的结点那就是n结点*/
if (c == 0) {
//如果onlyIfAbsent为true,或者尝试CAS的将n的的value从v改成value成功
if (onlyIfAbsent || n.casValue(v, value)) {
//返回v,doPut方法结束
@SuppressWarnings("unchecked") V vv = (V) v;
return vv;
}
//到这一步,说明:
//onlyIfAbsent为false并且CAS替换value失败,说明存在并发操作
//break跳出内层循环,继续外层循环,相当于重新开始内层循环,重新查找b、n
break; // restart if lost race to replace value
}
//到这一步,说明:
//c小于0,此时没有key相等的结点,即n.key > key > b.key,此时需要新数据结点
// else c < 0; fall through
}
/*
* 到这一步,说明:
* n为null,即遍历到了数据链表的最后一个结点,即目前的所有存在的key都小于指定的key
* 或者 key小于n的key,即n.key > key > b.key
* 以上两种情况下满足任意一种,此时都需要插入新数据结点
*/
//新建一个数据结点z,后继为n
z = new Node<K, V>(key, value, n);
//尝试CAS的将b的next引用从n改为z
if (!b.casNext(n, z))
//如果CAS改变引用失败
//break跳出内层循环,继续外层循环,相当于重新开始内层循环,重新查找b、n
break; // restart if lost race to append to b
//到这里,说明添加成功, b -> n 改为 b -> z -> n
// break outer直接跳出外层死循环,那么这里的内外死循环都彻底环结束,进入到下一个阶段
break outer;
}
}
/*
* 2 到这一步,一定是添加结点成功了,进入第二步
* 使用随机数算法判断是否增加索引结点
* 如果是,那么后续又分为两小步;如果不是那么直接返回null
**/
//首先通过ThreadLocalRandom.nextSecondarySeed()获取一个取得线程无关的伪随机数种子rnd
//既然都是伪随机数,为什么不使用Random来获取随机数呢?因为Random为了保证线程安全要求所有线程使用CAS竞争同一个种子,失败则自旋重试
//这样大大降低了并发性能,而ThreadLocalRandom类似于ThreadLocal,每个线程维护自己的一个普通long类型的种子变量
//每个线程生成随机数时候根据自己老的种子计算新的种子,并使用新种子更新老的种子,,就不会存在竞争问题,这会大大提高并发性能。
int rnd = ThreadLocalRandom.nextSecondarySeed();
//0x80000001是十六进制,转换为二进制就是1000 0000 0000 0000 0000 0000 0000 0001
//rnd & 0x80000001如果结果为0,说明rnd的最高位符号位和最低位都是0,那么可以增加索引
//实际上最高位符号位和最低位都是0的数就是正偶数,否则实际上rnd就是负数或者正奇数,
//即如果rnd为正偶数,那么可以增加索引层级;如果是负数或者正奇数,那么就直接结束方法,这看起来比较随机的
//首先获取一个随机数rnd,然后判断rnd是否是正偶数来判断是否需要增加索引,这就是随机数算法,看起来还挺简单的
//如果结果不为0,那么这个if语句块就跳过了,直接到最下面返回null,doPut方法结束
if ((rnd & 0x80000001) == 0) { // test highest and lowest bits
/*
* 2.1 增加索引结点或者向上增加索引层级,同时构建新增索引结点之间的纵向引用关系(right、node)以及新增层级的新增索引结点的横向引用关系(right)
* 进入if代码块意味着确定是需要增加索引结点
* 然后判断需要增加几个索引结点,以及是否需要再增加一个索引层级
* 如果仅仅需要再目前的某些索引层级上增加几个索引结点,那么就新增索引结点,然后构建索引结点之间的纵向引用关系down以及与底层数据结点的引用关系node
* 如果除了新增索引结点还需要再增加一个索引层级,那么先类似于上面新增索引结点,构建索引结点之间的纵向引用关系down以及与底层数据结点的引用关系node,并保存在一个数组idxs中,
* 随后至少新增一层索引链表:创建新的头索引结点然后更新head指向该结点,并且构建新增的头索引和idxs数组对应的索引结点之间的right关系(注意已经存在的索引层级的结点之间的right之间的关系还是没有构建)
*/
//level初始化为1,level将代表基于老的索引链表新增的索引结点的最大索引层级
int level = 1, max;
//判断rnd的二进制低位有多少个连续的1(排除最低位),level就自增多少次,这就是计算具体有多少层级的方法,这也是随机数算法
while (((rnd >>>= 1) & 1) != 0)
++level;
//初始化idx为null,idx将代表基于老的索引层级新增的索引结点中的层级最高的结点
Index<K, V> idx = null;
//获取此时最顶层索引链表的的head赋给h,h代表最新的顶层索引链表头结点
HeadIndex<K, V> h = head;
/*
* 如果level小于等于目前跳表的最大层级
* 那么只需要增加索引结点,不需要增加索引层级
*/
if (level <= (max = h.level)) {
//那么在[1,level]的索引链表的对应位置均插入一个索引结点,但是不需要新增索引层级。
for (int i = 1; i <= level; ++i)
//新建Index索引结点,node指向z,down指向idx(低一级的对应索引结点),right引用关系暂时为null
//即先确定纵向引用关系
//在循环之中不断为idx赋值,可知在循环完毕之后idx最终指向最上层的需要新增的索引结点
//即纵向引用关系的链表头,即基于老的索引层级新增的索引结点中的层级最高的结点
idx = new Index<K, V>(z, idx, null);
}
/*
* 否则,表示level大于目前跳表的最大层级max
* 那么需要增加索引结点和增加至少一层索引层级
*/
else { // try to grow by one level
//level等于max+1,尝试增长一个级别
level = max + 1; // hold in array and later pick the one to use
//构造索引结点数组idxs,长度为level+1,idxs[0]不会被使用
@SuppressWarnings("unchecked") Index<K, V>[] idxs =
(Index<K, V>[]) new Index<?, ?>[level + 1];
//从1开始遍历level
for (int i = 1; i <= level; ++i)
//类似于if中的代码,循环新建索引结点以及结点的纵向关系,
//同时还将结点存入数组的对应索引位置,比如数组索引为1,
//那么该位置存入的Index结点就是将要与第一级索引链表(level=1)构建横向引用关系(right)的索引结点。
//在循环之中不断为idx赋值,可知在循环完毕之后idx最终指向最上层的新增的索引结点,即纵向引用关系的链表头
idxs[i] = idx = new Index<K, V>(z, idx, null);
/*死循环,尝试进行新索引层级的构建*/
for (; ; ) {
//获取此时最新的head赋给h
h = head;
//获取旧的层级
int oldLevel = h.level;
//如果level此时小于等于oldLevel,那说明在此其间被其他线程增大了层级
if (level <= oldLevel) // lost race to add level
//那么该线程也不需要重复增加了层级,直接break跳出循环,到下一步
break;
//有可能此时层级没变,还有可能在此期间其他线程减小了层级
//newh首先保存h指向的索引结点
HeadIndex<K, V> newh = h;
//oldbase保存h指向的索引结点关联的数据结点,即数据链表头结点
Node<K, V> oldbase = h.node;
//循环遍历、创建新增的层级,如果此时的oldLevel比以前的层级更小,那么此时创建的层级可能超过1层
for (int j = oldLevel + 1; j <= level; ++j)
//首先新增一个头索引结点,node指向oldbase,down指向newh,right指向idxs数组对应的索引结点,j就是头索引结点保存的层级
//最终newh将指向新增的最顶层的的索引链表的头结点
newh = new HeadIndex<K, V>(oldbase, newh, idxs[j], j);
//CAS的更新head指向最新的最顶层的索引链表的头索引结点
if (casHead(h, newh)) {
//CAS成功之后
//h指向newh,即新增的最顶层的的索引链表的头结点
h = newh;
//level指向oldLevel,即基于老的索引链表新增的索引结点的最大索引层级
//idx指向 基于老的索引层级新增的索引结点中的层级最高的结点
idx = idxs[level = oldLevel];
//break跳出循环,添加新增层级和相关索引结点的纵向索引结点成功
break;
}
//CAS失败之后,继续下一次循环
}
}
/*
* 2.2 构建原索引层级的结点和新增结点之间的横向引用关系(right)
* 到这里,说明上面增加索引所结点或者增加索引层级成功,但是但存在一些索引链表内的right横向的引用关系没有构建
* 下面的代码将尝试构建新索引结点和对应索引链表的结点之间的right引用关系,即横向引用关系
*/
// find insertion points and splice in
//insertionLevel赋值为level,代表基于老的索引链表新增的索引结点的最大索引层级
splice:
for (int insertionLevel = level; ; ) {
//获取h.level赋值给j,这时的h可能已经不是真的head了
int j = h.level;
//初始化 q=h,即顶层索引链表头结点;r = q.right,即顶层索引链表头结点的后继
//初始化t = idx,即基于老的索引层级新增的索引结点中的层级最高的结点
/*
* 这一个for循环尝试查找 在当前索引层级之中小于指定key的最大索引结点q
* 一部分就类似于findPredecessor方法的代码
*/
for (Index<K, V> q = h, r = q.right, t = idx; ; ) {
//如果这时q等于null或者t等于null,那说明出现了并发操作导致结点被删除
//实际上几乎不可能发生的情况,这里有一个疑问?或许只是为了代码的健壮性?
if (q == null || t == null)
//那么直接break结束最外层循环,doPut方法结束
break splice;
//如果r不为null,说明q存在后继,即还没有到当前索引链表的尾部
if (r != null) {
//获取r对应的node数据结点n
Node<K, V> n = r.node;
// compare before deletion check avoids needing recheck
//比较key和n.key的大小获取结果c
int c = cpr(cmp, key, n.key);
//n的值为null,说明n被删除,那么帮助删除索引结点,和findPredecessor方法中的逻辑是一样的
if (n.value == null) {
if (!q.unlink(r))
//如果移除失败,那么break跳出内层循环,继续外层循环,即重新初始化数据进入内层循环
//重新从head开始查找
break;
//成功之后,获取此时q的right赋值为r
r = q.right;
//结束本次内层循环,继续下一次内层循环,这里r的值变了,因此下一次循环还是在本层的索引链表中查找
continue;
}
//如果c大于0,说明指定key大于n的key,那么还需要向后查找横向插入位置
if (c > 0) {
//q等于r
q = r;
//r等于r.right
r = r.right;
//结束本次内层循环,继续下一次内层循环,这里q、r的值都变了,相当于向后移动了一个结点
continue;
}
}
/* 到这一步,说明:
* r等于null,即q是索引链表的最后一个结点
* 或者 r对应的数据结点n的value不为null 并且 指定key小于等于r对应的数据结点n的key,q肯定就是当前索引层级的小于指定key的最大结点
*
* 判断j是否等于insertionLevel,如果j==insertionLevel为true,那么说明j终于和insertionLevel同步了
* 即说明当前层次就是 基于老的索引层级中新增了索引结点的索引层级,此时可以进行新增索引结点的横向关系的构建
*/
if (j == insertionLevel) {
/*
* q调用link方法,尝试CAS的更新right横向引用关系
* 引用变更: q -> r 变成 q -> t -> r
*/
if (!q.link(r, t))
//如果CAS失败,那么break跳出内层循环,继续外层循环,即重新初始化数据进入内层循环
break; // restart
//更新引用关系成功之后,如果t对应结点的value为null,说明新插入的结点被删除了,那么直接结束横向引用的构建操作,因为没有意义了
if (t.node.value == null) {
//调用findNode查找key对应的结点,但这里调用findNode作用是清除前面的value为null的结点
findNode(key);
//那么直接break结束最外层循环,doPut方法结束
break splice;
}
/*
* 每一次的构建当前层级的新索引结点的横向关系成功之后,insertionLevel必须自减1
* 如果insertionLevel自减1之后为0,说明此时已经循环到了最底层的索引链表,并且新增索引结点的横向引用关系已经构建完毕
* 那么直接break结束最外层循环,doPut方法结束
*/
if (--insertionLevel == 0)
//直接break结束最外层循环,doPut方法结束,这是构建横向引用关系的死循环中唯一正确的出口
break splice;
//否则,将会继续下一次循环,构建下一层索引链表的新增结点的横向引用关系
}
/*
* 每一次循环j必须自减1,相当于降低层级,同时也是为了和insertionLevel的值进行同步,如果同步了说明遍历到了 基于老的索引层级中新增了索引结点的索引层级
* 如果j自减1之后 大于等于 此时的insertionLevel并且 j 小于 level,
* 即小于基于老的索引链表新增的索引结点的最大索引层级,说明此时j和insertionLevel已经同步了。
*/
if (--j >= insertionLevel && j < level)
//那么t指向t对应的下层索引结点,即下一个层级的新增索引结点。
t = t.down;
/*到此本层级的索引链表的横向引用关系 构建完毕 或者 没有构建,开始处理下一层的索引链表的横向引用关系*/
//q等于q对应的下层索引结点
q = q.down;
//r等于q的后继
r = q.right;
//继续下一次循环
}
}
}
//第二步完成,最终返回null
return null;
}
/**
* Index结点类中的方法
* 尝试更新索引结点的right横向right引用关系
*
* @param succ 预期的当前结点的后继
* @param newSucc 新的后继
* @return true 成功 false 失败
*/
final boolean link(Index<K, V> succ, Index<K, V> newSucc) {
//n赋值为此时的node
Node<K, V> n = node;
//新的后继结点的后继指向原来的后继
newSucc.right = succ;
//如果此时node的value不为null,那么尝试CAS的设置当前结点的right后继从succ变成newSucc
//如果两个表达式都为true,那么表示当前结点没被删除并且更新right横向引用关系成功
return n.value != null && casRight(succ, newSucc);
}
复制代码
3.4.1.1. 执行详情分析
上面的大概步骤主要分为两步,第一部是新增数据结点或者替换value,第二步就是构建关联新结点的索引关系。下面我们来详细讲解它们的步骤!
3.4.1.1.1 新增数据结点或者替换value
大概步骤为:
- 开启一个死循环:
- 调用findPredecessor方法查找小于指定key的具有索引关系的最大Node结点,没找到就返回数据链表头结点,返回值赋值为b,然后n=b.next,即后继,此时n对应的数据结点的key肯定是大于等于指定key的。
- 内部再开启一个死循环: 1. 如果n不为null,说明b的后继不为null,那么对n进行一系列校验,如果为null,那么进入下一步; 1. f=n.next,即f为n的后继; 2. 如果此时n不等于b.next,因为ConcurrentSkipListMap没有锁,因此可能在这里的b的后继发生了改变,比如有另一个线程抢先一步在b后面插入了结点。那么break跳出内层循环,继续外层循环,相当于重新开始内层循环,重新查找b、n。 3. 如果n的value为null,说明n结点的数据被删除了,但是后续引用关系没有删除。n调用Node结点的helpDelete方法帮助删除数据结点,帮助成功之后,break跳出内层循环,继续外层循环,相当于重新开始内层循环,重新查找b、n。 4. 如果b的value为null,或者 v等于n,即n是一个标记结点,说明b结点的数据被删除了,但是后续引用关系没有删除。break跳出内层循环,继续外层循环,相当于重新开始内层循环,重新查找b、n。 5. 到这里,说明前面的校验通过,b和n对应的数据结点都没有被删除。最开始的findPredecessor方法是查找小于指定key的具有索引关系的最大Node结点,但同时可能存在与key更加接近的但是没有索引关系的结点,这里需要查找出来,这一步就是在数据链表中查找真正的小于或者等于key的最大结点。调用cpr将指定key和n的key相比较获取结果c,如果指定key大于n的key,说明还存在比b更大的小于指定key的结点。那么b = n,n = f,即改变b、n的引用,都向后移动一位,然后continue结束本次内层循环,继续下一次循环,直到找到真正的小于等于key的最大结点。 6. 如果c==0,说明找到一个key相等的结点那就是n结点。这里就走替换逻辑了。如果onlyIfAbsent为true,或者尝试CAS的将n的的value从v改成value成功,返回旧v,doPut方法结束。 如果失败那么说明onlyIfAbsent为false并且CAS替换value失败,说明存在并发操作break跳出内层循环,继续外层循环,相当于重新开始内层循环,重新查找b、n。 7. 到这一步,说明:c小于0,此时没有key相等的结点,即n.key > key > b.key,此时需要插入新结点。 2. 到这一步,说明:n为null,即遍历到了数据链表的最后一个结点,即目前的所有存在的key都小于指定的key,或者key小于n,即n.key > key > b.key,以上两种情况下满足任意一种,此时都需要插入新数据结点。 3. 新建一个数据结点z,后继为设置为n,尝试CAS的将b的next引用从n改为z。如果CAS改变引用失败,break跳出内层循环,继续外层循环,相当于重新开始内层循环,重新查找b、n。 4. 如果CAS成功,说明添加成功,数据链表的关系由 b -> n 改为 b -> z -> n。break outer直接跳出外层死循环,那么这里的内外死循环都彻底环结束,进入到下一个阶段。
可以看到,新增数据结点的逻辑还算比较简单。首先是调用findPredecessor方法查找小于指定key的具有索引关系的最大Node数据结点,然后从该数据结点的下一个结点开始继续循环查找小于指定key的数据结点,如果找到了key相等的结点,那么替换value,返回旧值,整个doPut方法就结束了。如果没有找到key相等的结点,那么最终后再合适的位置(如果key大于所有已存在的key那么就在数据链表最后最后,或者在两个结点之间)插入新结点。
在findPredecessor方法过程中会帮助清除遍历到的正在被删除(value为null)的数据结点关联的索引结点,循环遍历底层数据链表的过程中,会帮助清除遍历到的正在被删除(value为null)的数据结点,这两个都是帮助删除的逻辑。
新增数据结点或者替换value在一个死循环中进行的,因为结点的更改都是使用的CAS,因此可能由于并发操作而失败,因此需要不断的循环重试,直到最终成功。这样可以避免锁的调用造成线程阻塞唤醒带来的线程上下文切换的时间开销,但同时可能由于持续空转(自旋/循环)而给CPU带来很大占用开销。
3.4.1.1.2 随机数算法计算索引结点
在新增结点成功之后,然后通过随机数算法生成的随机数是否符合规则来判断是否需要增加索引结点。 主要代码如下:
/*
1. 随机数算法判断是否增加索引结点;如果是,那么里面又分为两小步;如果不是那么直接返回null
*/
int rnd = ThreadLocalRandom.nextSecondarySeed();
if ((rnd & 0x80000001) == 0) {
//…………后续两个小步骤
}
复制代码
首先通过ThreadLocalRandom.nextSecondarySeed()获取一个取得线程无关的伪随机数种子rnd。既然都是伪随机数,为什么不使用Random来获取随机数呢? 实际上Random也能保证线程安全,但是Random为了保证线程安全要求所有线程使用CAS竞争同一个种子,失败则自旋重试这样大大降低了并发性能,而ThreadLocalRandom类似于ThreadLocal,每个线程维护自己的一个普通long类型的种子变量每个线程生成随机数时候根据自己老的种子计算新的种子,并使用新种子更新老的种子,就不会存在竞争问题,这会大大提高并发性能。
然后使用(rnd & 0x80000001) 并判断结果是否为0来决定是否新增关联的索引结点。 0x80000001是十六进制,转换为二进制就是1000 0000 0000 0000 0000 0000 0000 0001。rnd & 0x80000001如果结果为0,说明rnd的最高位符号位和最低位都是0,那么可以增加索引结点。实际上最高位符号位和最低位都是0的数就是正偶数,否则实际上rnd就是负数或者正奇数,即如果rnd为正偶数,那么可以增加索引层级;如果是负数或者正奇数,那么就直接结束方法,这看起来比较随机的。
首先获取一个随机数rnd,然后判断rnd是否是正偶数来判断是否需要增加索引结点,这就是随机数算法,看起来还挺简单的。 如果结果不为0,那么这个if语句块就跳过了,直接到最下面返回null,doPut方法结束,如果为0,那么进入下一步开始构建关联新结点的索引关系。
3.4.1.1.3 构建关联新结点的索引关系
在上面的随机数算法判断需要新增索引结点之后,那么开始构建关联新结点的索引关系,这里可分为两步,第一步创建索引结点并且构建索引结点的纵向引用关系,以及新增索引层级并构建新增层级索引结点的横向引用关系。第二步就是构建原来已存在的索引层级和新增结点之间的横向引用关系。
3.4.1.1.3.1 构建纵向索引关系
大概步骤为:
- 初始化变量level=1,level将代表基于老的索引链表新增的索引结点的最大索引层级。
- 判断rnd的二进制低位有多少个连续的1(排除最低位),level就循环自增多少次,这就是计算具体需要有多少个层级的索引结点的方法,这也是随机数算法。
- 初始化idx为null,idx将代表基于老的索引层级新增的索引结点中的层级最高的结点。获取此时最顶层索引链表的的head赋给h。
- 如果level小于等于目前跳表的最大层级即(max = h.level),那么说明需要在[1,level]的索引链表的对应位置均插入一个索引结点,但是不需要新增索引层级:
- 循环新建Index索引结点,这里的索引结点之间只构建了和底层数据结点的next的关系以及与下一层新索引结点的down的关系,即纵向引用关系,但是每一个新索引结点和对应的索引层级链表之间的横向引用关系(right)并没有建立。在循环之中不断为idx赋值,可知在循环完毕之后idx最终指向最上层的新增的索引结点,即纵向引用关系的链表头,即基于老的索引层级新增的索引结点中的层级最高的结点。然后进入第二小步,构建横向引用关系。
- 如果level大于目前跳表的最大层级即(max = h.level),那么说明需要增加索引结点和同时增加至少一层的索引层级:
- level=max+1,尝试增长一个新索引级别。
- 构造Index索引结点数组idxs,长度为level+1,idxs[0]不会被使用。
- 从1开始遍历level,类似于if中的代码,循环新建索引结点以及结点的纵向关系,横向引用同样没有构建,但同时还将结点存入数组的对应索引位置,比如数组索引为1,那么该位置存入的Index结点就是将要与第一级索引链表(level=1)构建横向引用关系(right)的索引结点。在循环之中不断为idx赋值,可知在循环完毕之后idx最终指向最上层的新增的索引结点,即纵向引用关系的链表头。
- 然后开启一个死循环,进行新索引层级的构建: 1. 再次获取此时最新的head赋给h,获取h.level赋值给oldLevel; 2. 判断如果level此时小于等于oldLevel,那说明在此其间被其他线程增大那么该线程也就不需要重复增加了,直接break跳出循环,然后进入第二小步,构建横向引用关系。 3. 如果level此时大于oldLevel(有可能此时层级没变,那么还有可能在此期间其他线程减小了层级)。newh首先保存h指向的索引结点,oldbase保存h指向的索引结点关联的数据结点,即数据链表头结点。 4. 循环遍历、创建新增的层级,如果此时的oldLevel比以前的层级更小,那么此时创建的层级可能超过1层,但是至少新增一层: 1. 新增HeadIndex头索引结点,node指向oldbase,down指向newh,right指向idxs数组对应的索引结点,j就是头索引结点保存的层级。 2. 循环中不断为newh赋值,最终newh将指向新增的最顶层的的索引链表的头结点。循环时还为HeadIndex构建了right的横向引用关系,即指向新增的索引结点。 5. CAS的更新head指向最新的最顶层的索引链表的头索引结点,如果成功:h又指向newh,即新增的最顶层的的索引链表的头结点。level指向oldLevel,即基于老的索引链表新增的索引结点的最大索引层级,idx再次指向 idxs[level],即基于老的索引层级新增的索引结点中的层级最高的结点,最后break跳出循环,添加新增层级和相关索引结点的纵向索引结点成功,然后进入第二小步,构建横向引用关系。 6. CAS更新失败之后,继续下一次循环重试!
第一步执行完毕,此时已经新建了全部需要新增的索引结点,并且构建了好了纵向引用关系(node、down),但是没有构建每个索引结点的横向引用关系(right),另外还可能新增了索引层级,并且对于新增层级的头索引结点HeadIndex都建好了所有的引用关系(node、down、right、level)。
在第一步执行完毕之后,我们来分析level、idx、h都指向了哪里,最终代表什么意思,以level和h.level的关系,因为它们在第二小步构建横向索引的时候都会用到:
- 首先计算level的值,这里level代表基于老的索引链表新增的索引结点的最大索引层级,idx为null,h代表目前的顶层索引链表头结点。
- 后面如果计算的level小于等于h.level,那么进入if新增索引结点就行了,然后就到第二小步,此时 level <= h.level。最终,level代表基于老的索引链表新增的索引结点的最大索引层级,idx在循环赋值之后将代表基于老的索引层级新增的索引结点中的层级最高的结点,即纵向引用关系的链表头,h代表目前的顶层索引链表头结点。level <= h.level。
- 否则,进入else。在else的代码中有两个出口:
- 首先level = max + 1 即 level = h.level + 1,此时level表示新增一个层级之后的最大层级,然后再循环中为idx赋值,之后idx将代表最上层的新增的索引结点,即纵向引用关系的链表头。然后开启死循环,此时h = head,即h又代表目前最新的顶层索引链表头结点。
- 如果此时level <= oldLevel,说明在此其间被其他线程增大了层级,此时 level <= h.level那么该线程也不需要重复增加了层级,直接break跳出循环,到下一步。最终,level代表在原最大层级之上新增一个层级之后的最大层级,这实际上也相当于基于老的索引链表新增的索引结点的最大索引层级,只不过新的层级被别的线程“帮助建立”了;idx代表最上层索引链表的新增的索引结点,即纵向引用关系的链表头,也相当于代表基于老的索引层级新增的索引结点中的层级最高的结点;h代表目前最新的顶层索引链表头结点。level <= h.level。
- 如果level > oldLevel(有可能此时层级没变,还有可能在此期间其他线程减小了层级),那么新增至少一个层级,新增完毕之后。最终,level = oldLevel,即代表基于老的索引链表新增的索引结点的最大索引层级;h=newh,即代表新增的最顶层的的索引链表的头结点;idx=idxs[level],即代表基于老的索引层级新增的索引结点中的层级最高的结点,这里就不是纵向引用关系的链表头了。level < h.level。
实际上,最终level代表基于老的索引链表新增的索引结点的最大索引层级;idx代表基于老的索引层级新增的索引结点中的层级最高的结点;h代表目前的顶层索引链表头结点。level < = h.level。
3.4.1.1.3.2 构建横向索引关系
这一步用于构建新加的索引结点之间的横向引用关系(right)。
首先我们需要明白上面的几个参数有什么意义!我们知道,如果在上一步新增了层级,那么实际上这个新增的层级就两个结点,一个头索引结点,一个索引结点,头索引结点在建立的时候已经建立了和后面的索引结点的right关系,而此时新索引结点在新的索引层级中只有一个索引结点,因此不需要构建由新索引结点到后继结点的right引用关系。
因此,实际上只有在新添加索引结点的时候对于已经存在的索引链表,此时新添加的索引结点才需要构建横向引用关系,而此时就需要用到level和idx了。
第二步的大概步骤为:
- 初始化insertionLevel = level,同样开启一个死循环:
- 获取h.level赋值给j;
- 初始化 q=h,即顶层索引链表头结点;r = q.right,即顶层索引链表头结点的后继;初始化t = idx,即基于老的索引层级新增的索引结点中的层级最高的结点,内部开在起一个for死循环,首先尝试查找在当前索引层级之中小于指定key的最大索引结点q,一部分就类似于findPredecessor方法的代码,找到之后对于该层索引链表对应的新索引结点构建横向关系,然后切换到下一层,直到level=1的索引链表引用关系构建完毕之后退出该步骤; 1. 如果这时q等于null或者t等于null,那么直接break结束最外层循环,doPut方法结束,返回null;实际上几乎不可能发生的情况,因此这里有一个疑问?或许只是为了代码的健壮性? 2. 如果r不为null,说明q存在后继,即还没有到当前索引链表的尾部,进行校验: 1. 获取r对应的node数据结点n; 2. 调用cpr比较key和n.key的大小,获取结果c; 3. 如果n的值为null,说明正在n被删除,那么帮助删除该索引结点,和findPredecessor方法中的逻辑是一样的;如果帮助失败,那么break跳出内层循环,继续外层循环,即重新初始化数据进入内层循环;成功之后,获取此时q的right赋值为r,结束本次内层循环,继续下一次内层循环,这里只有r的值变了,因此下一次循环还是在本层的索引链表中查找。 4. 如果c大于0,说明指定key大于n的key,那么还需要向后查找横向插入位置,q = r,r = r.right,结束本次内层循环,继续下一次内层循环,这里q、r的值都变了,相当于向后移动了一个结点。 3. 到这一步,说明:r等于null,即q是索引链表的最后一个结点,或者 r对应的数据结点n的value不为null,并且 指定key小于等于r对应的数据结点n的key,那么q肯定就是当前索引层级的小于指定key的最大结点。此时说明找到了位置,可以尝试开始构建横向索引。 4. 判断j是否等于insertionLevel,如果j==insertionLevel为true,那么说明j终于和insertionLevel同步了。即说明当前层次就是基于老的索引层级中新增了索引结点的索引层级,此时可以进行新增索引结点的横向关系的构建,因为j>=insertionLevel,对于大于insertionLevel的层级(不算做旧的索引链表),不需要构建横向引用: 1. q调用link方法,尝试CAS的更新right横向引用关系,从q -> r 变成 q -> t -> r。如果CAS失败,那么break跳出内层循环,继续外层循环,即重新初始化数据进入内层循环。 2. CAS成功之后,如果此时发现t对应数据结点的value为null,说明刚刚新插入的结点又被删除了。那么调用一次findNode清除前面的value为null的结点,然后直接break结束最外层循环,结束所有的横向引用的构建操作,因为没有意义了,而上面已经构建好了的横向关系,将会被后来的其他线程删除。doPut方法结束,返回null。 3. 每一次的构建当前层级的新索引结点的横向关系成功之后,insertionLevel必须自减1。如果insertionLevel自减1之后为0,说明此时已经循环到了最底层的索引链表,并且新增索引结点的横向引用关系已经构建完毕,那么直接break结束最外层循环,doPut方法结束,返回null。这是构建横向引用关系的死循环中唯一正确的出口。 5. 每一次循环j必须自减1,相当于降低层级,同时也是为了和insertionLevel的值进行同步,如果同步了说明遍历到了 基于老的索引层级中新增了索引结点的索引层级。如果j自减1之后 大于等于 此时的insertionLevel并且 j 小于 level,即小于基于老的索引链表新增的索引结点的最大索引层级,说明此时j和insertionLevel已经同步了。那么t指向t对应的下层索引结点,即下一个层级的新增索引结点。 6. 到此本层级的索引链表的横向引用关系 构建完毕或者没有构建,开始处理下一层的索引链表的横向引用关系,q等于q对应的下层索引结点,r等于q的后继,然后进行下一次内层循环。
可以发现,构建新索引结点和对应索引链表之间的横向引用关系还是比较简单的。就是从最顶层开始遍历,找到小于指定key的最大索引结点,然后判断此时的层级是否到了基于老的索引层级中新增了索引结点的索引层级,如果是,那就可以尝试构建该层级的索引关系了,构建完毕之后在判断如果当前层级就是level=1的索引层级,表示横向引用关系构建完毕,此时可以退出第二步。佛祖儿,继续切换到下一层级。
实际上最难以理解的就是如何找到基于老的索引层级中新增了索引结点的索引层级,这时就用到了在第一步中的参数level(insertionLevel),最高层级j每次循环降低一级,如果某次循环j之时,发现j等于insertionLevel,那就说明遍历到了基于老的索引链表新增的索引结点的最大索引层级,而此时idx也等于基于老的索引层级新增的索引结点中的层级最高的结点,这样就对应上了,此后insertionLevel和j都会一起递减的向下构建。
3.4.1.2 findPredecessor查找“前驱”结点
实际上我们的ConcurrentSkipListMap结构中的索引链表和数据链表都是单链表,只保存了后继结点的引用关系,并没有“前驱”引用关系。
&emsp**;这里的findPredecessor用于查找小于指定key的具有索引关系的最大Node结点,没找到就返回数据链表头结点,也就相当于“前驱”结点的含义了!但是这里返回的前驱结点要求必须建立了索引关系,因此和常说的前驱又不一样!**
主要步骤为:
- 对key进行null校验。
- 开启一个死循环:
- 初始化一些数据,q = head,为最新的head,r = q.right,为q的后继索引结点,d = null;
- 再开启一个死循环: 1. 如果r不为null,说明当前索引结点存在后继,即还没有到当前索引链表的尾部: 1. 获取r关联的的最底层数据结点n,获取n的key->k。 2. 如果n的value为null,说明n正在删除,那么帮助删除n在该层索引链表的关联的索引结点。如果帮助删除失败,那么break结内层循环,进行下一次外层循环,如果帮助删除成功,r = q.right,continue结束本次内层循环,继续下一次内层循环。 3. 到这一步,说明n没有被删除,那么调用cpr方法对key和k进行比较,如果结果大于0,说明指定key大于r关联的的最底层数据结点的key,q = r; r = q.right,然后continue结束本次内层循环,继续下一次内层循环。如果结果小于等于0,那么进入下一步。 2. 到这一步,说明:r等于null,即q是索引链表的最后一个结点,或者 n的value不为null 并且 指定key小于等于r对应的结点的key,但是肯定大于q对应的数据结点的key,并且此时q对应的索引结点就是当前层级的索引链表中的小于指定key的最大索引结点,即当前索引链表查找完毕。以上两种情况都需要尝试切换到下一层索引链表继续查找。 3. d赋值为q.down,即d赋值为q关联的下一层索引链表的索引结点。如果d为null,说明当前索引链表刚好位于底层数据链表之上,那么返回q.node,即返回q关联的底层数据结点,findPredecessor方法结束。 4. 到这一步,说明当前索引链表不是位于底层数据链表之上的链表,即下面还有更底层的索引链表那么继续改变q和r的指向:q = d; r = d.right,这相当于切换到下一层,然后继续下一次内层循环,下一次的循环将会遍历下一层的索引链表。
/**
* 查找小于指定key的具有索引关系的最大Node结点,没找到就返回数据链表头结点
* 从最顶层的索引链表头结点head开始,一层一层向下查找小于指定key的最大Node,直到找到最底层的索引链表位置,
* 然后返回索引结点对应的数据结点,如果没找到最终会返回数据链表的头结点。
*
* @param key 指定key
* @return 小于指定key的具有索引关系的最大Node结点,没找到就返回数据链表头结点
*/
private Node<K, V> findPredecessor(Object key, Comparator<? super K> cmp) {
//1 key校验
if (key == null)
throw new NullPointerException(); // don't postpone errors
/*2 开启一个死循环*/
for (; ; ) {
/*
* 3 内部再开启一个循环,初始化一些数据:
* q = head,为最新的head,第一次遍历的时候就是最上层索引链表的头结点
* r = q.right,为q的后继索引结点
* d = null,在循环中将会为保存q.down指向的结点
*/
for (Index<K, V> q = head, r = q.right, d; ; ) {
//3.1 如果r不为null,说明当前索引结点存在后继,即还没有到当前索引链表的尾部
if (r != null) {
//获取r关联的最底层数据结点n
Node<K, V> n = r.node;
//获取n的key
K k = n.key;
/*
* 3.1.1 如果n的value为null,说明n正在删除,那么帮助删除n在该层索引链表的关联的索引结点
* 这个if相当于帮助删除索引结点的逻辑,注意这里帮助将本层的right引用删除了,其他层的引用没管
*/
if (n.value == null) {
//尝试移除r结点在当前索引链表的关系,即right引用关系
if (!q.unlink(r))
//如果移除失败,那么break跳出内层循环,继续外层循环,即重新开始内层循环
//重新从head开始查找
break; // restart
//成功之后,获取此时q的right赋值为r
r = q.right; // reread r
//结束本次内层循环,继续下一次内层循环,这里r的值变了,因此下一次循环还是在本层的索引链表中查找
continue;
}
/* 3.1.2 到这一步,说明n没有被删除,那么调用cpr方法对指定key和r对应的结点的key进行比较
* 如果结果大于0,说明指定key大于被比较的key
*/
if (cpr(cmp, key, k) > 0) {
//q赋值为r
q = r;
//r赋值为r.right后继
r = r.right;
//结束本次内层循环,继续下一次内层循环,这里q、r的值都变了
continue;
}
//到这一步,说明指定key小于等于r对应的结点的key,进入3.2
}
/* 3.2 到这一步,说明:
* r等于null,即q是索引链表的最后一个结点
* 或者 n的value不为null 并且 指定key小于等于r对应的结点的key,但是肯定大于q对应的数据结点的key,
* 并且此时q对应的索引结点就是当前层级的索引链表中的小于指定key的最大索引结点
*/
//d赋值为q.down,即d赋值为q关联的下一层索引链表的索引结点
//如果d为null,说明当前索引链表刚好位于底层数据链表之上
if ((d = q.down) == null)
//返回q.node,即返回q关联的底层数据结点
//这是该方法的唯一正常返回出口
return q.node;
/*
* 3.3 到这一步,说明:
* 当前索引链表不是位于底层数据链表之上的链表,即下面还有更底层的索引链表
* 那么继续改变q和r的指向
*/
//那么q赋值为d
q = d;
//r赋值为d.right后继
r = d.right;
//上面的两个赋值操作,相当于降低了遍历的索引链表的层级,降低一级
//然后继续下一次内层循环,下一次的循环将会遍历下一层的索引链表
}
}
}
/**
* Index索引结点类中的方法
* 尝试从当前索引链表中移除指定succ结点
*
* @param succ 预期后继结点
* @return true 成功 false 失败
*/
final boolean unlink(Index<K, V> succ) {
//如果当前索引结点的value不为null,即没有删除
//那么尝试CAS的将它的right引用从succ设置为succ.right,即将succ从当前索引链表中移除
return node.value != null && casRight(succ, succ.right);
}
/**
* Index索引结点类中的方法
* CAS的为当前结点的right引用赋值
*
* @param cmp 预期right引用结点
* @param val 新的结点
* @return true 成功 false 失败
*/
final boolean casRight(Index<K, V> cmp, Index<K, V> val) {
return UNSAFE.compareAndSwapObject(this, rightOffset, cmp, val);
}
/**
* ConcurrentSkipListMap中的方法
* 使用比较器或自然排序(如果 null)对key进行比较。
*
* @param c 指定比较器
* @param x 指定key
* @param y 被比较的结点的key
* @return 0 相等; >0 指定key大于被比较的key ; <0 指定key小于被比较的key
*/
@SuppressWarnings({"unchecked", "rawtypes"})
static final int cpr(Comparator c, Object x, Object y) {
//如果指定比较器不为空,那么使用制定比较器的compare方法比较两个key(自定义排序)
//否则,key强转为Comparable类型,使用compareTo方法比较两个key(自然排序)
return (c != null) ? c.compare(x, y) : ((Comparable) x).compareTo(y);
}
复制代码
总结起来,这个查找实际上很简单,就是从最顶层的索引链表头结点head开始,一层一层向下查找小于指定key的具有索引关系的最大Node,直到找到最底层的索引链表位置,然后返回索引结点对应的数据结点,如果没找到最终会返回数据链表的头结点。
在查找过程中如果遇到某层级的索引结点对应的数据结点的value为null,那么说明该数据被删除了,此时先要辅助将被删除数据结点对应的索引结点从该索引链表中删除(只删除right关系)。
某个跳表的结构如下:
现在我们要查找小于等于key=15的具有索引关系的最大Node,我们的查找路线如下:
如图所示,实际上我们只比较(调用cpr)了3次,就找到了key=13的结点:
先在最顶层索引链表中找到key=1的结点进行第一次比较,15>1,因此向后查找;然后我们又找到了key=9的结点进行第二次比较,此时15>9,但是后面没有索引结点了,因此尝试向下查找,我们发现下面还是索引链表,因此转入到level1的索引链表进行查找;
在level1中我们找到了后面的key=13的结点进行第三次比较,发现15还是大于13,但是后面没有索引结点了,因此因此尝试向下查找,我们发现下面的链表就是数据链表了(down=null),此时我们就直接返回key=13的结点即可!到此查找完毕!
如果在结点13的后面还有一个14结点,但是没有索引关系,我们最后返回的还是13结点,这就是“小于指定key的具有索引关系的最大Node”的真正含义!
3.4.1.3 helpDelete辅助删除结点
helpDelete用于帮助删除线程将value为null的数据结点从数据链表中移除。
由于采用CAS无锁算法,而删除操作实际上分了几个步骤,因此可能第一步成功将value位置null之后,后续步骤失败或者没有来得及执行就切换了CPU时间片,此时如果其他线程比如put线程在遍历结点时发现正在删除某个结点(value==null),那么当前线程尝试帮助删除该结点。
当前线程会对删除的第二步或者第三步中的一个步骤进行帮助,而不是将全部步骤包揽了,这样实际上是有利于降低帮助线程之间的CAS冲突。
/**
* Node结点类中的方法
* 通过追加标记结点(第二步)或改变引用关系(第三步)来帮助删除数据结点,当遍历到某个数据结点的value为null时会调用此方法。
*
* @param b 被删除结点的前驱
* @param f 被删除结点的后继
*/
void helpDelete(Node<K, V> b, Node<K, V> f) {
/*
* 删除的逻辑实际上分以下几步:
* 1 首先将找到的数据结点i的value尝试CAS的置为null;
* 2 然后在i和i的后继j之间CAS的插入一个标记结点k作为后继,这个标记结点的value指向标记结点自己,next指向j
* 3 最后尝试CAS的将i的前驱h的next引用指向j,这样就将i和i的标记结点k移除了底层数据链表(清除了next关系,无法通过next引用到达)
* 4 然后调用一次findPredecessor方法,将被删除结点i对应的索引结点从各自的索引链表中清除right关系
* 5 虽然i的标记结点k的next还是指向了j,但是由于GC roots不可达,并且不能访问到,在后续的GC中将被回收
*/
/*
* 1 如果f还是等于后继next 并且 当前node结点还是等于前驱b的后继,那么说明第三步还没有做完
* 此时当前线程仅仅会对删除操作的第二步或者第三步进行帮助,而不是将全部步骤包揽了,这样实际上是有利于降低帮助线程之间的CAS冲突
*/
if (f == next && this == b.next) {
/*
* 1.1 如果f等于null或者f的value不为f,那么表示尚未加入标记结点标记
* 说明第二步还没成功
*/
if (f == null || f.value != f) // not already marked
//尝试将当前结点的next引用指向,从f变成一个新的标记结点,该标记结点的value指向自己,next指向f
//这里帮助第二步
casNext(f, new Node<K, V>(f));
/*
* 1.2 否则,表示已经标记,但是还没有改变引用关系
* 即第二步成功了,第三步没有成功
*/
else
//那么尝试将b的next从当前结点变成f.next
//这里帮助第三步
b.casNext(this, f.next);
}
}
复制代码
3.4.1.4 findNode查找结点
findNode主要用于根据指定key查找与key相等的Node结点,同时会调用helpDelete辅助删除在遍历过程中所有遇到的 value 为 null 的结点。
findNode在put(doPut)方法的插入了数据并且构建某个索引层级横向引用关系成功但此时新插入的结点又被删除的情况下会被调用。在remove(doRemove)方法中如果将value置为null成功之后的新增标记结点失败或者改变引用关系失败的情况下会被调用。在replace方法中也会被调用来尝试查找与指定key相等的结点。有趣的是,在get或者containsKey等读的方法的内部调用的doGet方法中反而没有调用该方法。实际上,基本上所有方法包括get、containsKey等方法在遍历数据链表过程中都帮助删除辅助结点,但是有些是自己调用的helpDelete方法而已。
这实际上相当于doPut方法的第一部分,区别是不会替换value也不会插入结点。
/**
* 根据指定key查找与key相等的Node结点,同时删除所有遇到的 value 为 null 的结点
* 这实际上相当于doPut方法的第一部分,区别是不会替换value也不会插入结点
*
* @param key 指定key
* @return 结点,或者null
*/
private Node<K, V> findNode(Object key) {
//key检验
if (key == null)
throw new NullPointerException(); // don't postpone errors
//cmp保存全局的比较器,可能为null
Comparator<? super K> cmp = comparator;
/*开启一个死循环,尝试查找结点*/
outer:
for (; ; ) {
/*
* 调用findPredecessor查找小于指定key的具有索引关系的最大Node结点,没找到就返回数据链表头结点,返回值赋值为b;n=b.next,即后继
* 内部再开启一个死循环
*/
for (Node<K, V> b = findPredecessor(key, cmp), n = b.next; ; ) {
Object v;
int c;
//如果n为null,说明b后面没有了结点,即整个跳表没有key和指定key相等的结点了
if (n == null)
//break outer直接跳出外层死循环,那么这里的内外死循环都彻底环结束,最后将返回null
break outer;
//f=n.next,即f为n的后继
Node<K, V> f = n.next;
//如果此时n不等于b.next,因为ConcurrentSkipListMap没有锁,因此可能在这里的b的后继发生了改变,比如有另一个线程抢先一步在b后面插入了结点
if (n != b.next) // inconsistent read
//break跳出内层循环,继续外层循环,相当于重新开始内层循环,重新查找b、n
break;
//如果n的value为null,说明n结点的数据被删除了,但是后续引用关系没有删除
if ((v = n.value) == null) { // n is deleted
//n调用Node结点的helpDelete方法帮助删除数据结点
n.helpDelete(b, f);
//帮助成功之后,break跳出内层循环,继续外层循环,相当于重新开始内层循环,重新查找b、n
break;
}
/*
* 如果b的value为null,
* 或者 v等于n,即n是一个标记结点
* 说明b结点的数据被删除了,但是后续引用关系没有删除
*/
if (b.value == null || v == n) // b is deleted
//这里没法帮忙了
//break跳出内层循环,继续外层循环,相当于重新开始内层循环,重新查找b、n
break;
/*
* findPredecessor方法是查找小于指定key的具有索引关系的最大Node结点,
* 这里继续调用cpr将指定key和n的key相比较获取结果c,
* 如果c等于0,说明指定key等于n的key,那么找到了
*/
if ((c = cpr(cmp, key, n.key)) == 0)
//直接返回n,findNode方法结束
return n;
//如果c小于0,说明指定key小于n的key
//那就是没有找到相同的key
if (c < 0)
//break outer直接跳出外层死循环,那么这里的内外死循环都彻底环结束,最后将返回null
break outer;
//到这里说明c大于0,即指定key大于n的key,那么需要继续向后查找
//改变b、n的引用,都向后移动一位,然后结束本次内层循环,继续下一次循环,
//直到找到真正的等于或者小于key的结点 或者 查找完毕也没找到。
b = n;
n = f;
}
}
//返回null
return null;
}
复制代码
3.4.2 执行流程
假设某个跳跃表的结构如下:
现在我们要插入(8,8)的键值对!
首先我们需要通过findPredecessor方法定位到key小于8的具有索引关系的最大结点b,以及n = b.next,f=n.next:
然后继续在数据链表中查找key小于8的具有索引关系的最大结点:
然后新建节点z,next指向n,然后调用b.casNext(n, z),插入数据结点:
到此数据结点算是插入完毕了,下面开始计算是否需要增加索引结点,获取一个随机数,如果随机数为1486,转换为二进制就是10111001110,那么需要添加索引结点,然后,计算level=4。发现大于目前的最大level(2),因此还需要增加层级。
首先是新建索引结点纵向链表,然后新增索引层级,然后level=max+1=3;因此索引链表为三个结点,next指向新增加数据结点z, down指向下一级的索引结点:
然后在一个死循环中构建新的索引层级,需要新建一层索引层级,首先建立一个HeadIndex头索引结点,然后node指向目前的数据链表头结点,down指向目前的head指向的结点,right指向最上层的索引结点,level赋值为3。然后再使得head指向最新的索引头结点。最后h指向最新的索引头结点;level=oldLevel,即基于老的索引链表新增的索引结点的最大索引层级;idx = idxs[level],即基于老的索引层级新增的索引结点中的层级最高的结点。
到此新层级增加完毕,新增层级的横向引用关系也构建好了,下面是最后一步,构建基于老的索引层级新增的索引结点的横向引用关系:
insertionLevel=level=2,j = h.level=3。
q = h,r = q.right,t = idx。开始第一次内层循环,从最上层索引链表开始,首先循环找到当前索引层级的小于指定key的最大结点q,以及后继r。
很明显在第一次循环中,j == insertionLevel为false,因此不需要构建横向引用。然后j自减1变成2,--j >= insertionLevel && j < level 为false,q = q.down,r = q.right,切换到下一层索引(下一次循环)。
同样首先循环找到当前索引层级的小于指定key的最大结点q,以及后继r。
此时,j == insertionLevel为true,因此需要构建横向引用。调用q.link(r, t)方法,q -> r 变成 q -> t -> r:
到此,实际上level2索引层级的横向关系构建完毕,然后判断--insertionLevel == 0为false,此时insertionLevel=1,即下面还有以一级索引。然后j自减1变成1,此时--j >= insertionLevel && j < level 为true,因此t = t.down。q = q.down,r = q.right,切换到下一层索引(下一次循环)。
同样首先循环找到当前索引层级的小于指定key的最大结点q,以及后继r。
此时,j == insertionLevel为true,因此需要构建横向引用。调用q.link(r, t)方法,q -> r 变成 q -> t -> r:
到此,实际上level1索引层级的横向关系构建完毕,然后判断--insertionLevel == 0为true,此时insertionLevel=0,即下面没有索引层级了。直接break结束最外层循环,doPut方法结束,put方法结束。此时的跳表结构如下:
以上只是在没有线程竞争以及没有遇到无效结点的情况,在高并发环境下的插入过程将会更加复杂。那可真是让人头大啊!
3.5 remove操作
3.5.1 remove(key)
public V remove(Object key)
如果此集合存在某个结点的key与指定key相等,那么从此集合中移除该结点。返回以前与指定key关联的value;如果该键没有映射关系,则返回 null。
如果指定key无法与Map中的结点的key进行比较,则抛出ClassCastException,如果指定key为 null,则抛出NullPointerException。
/**
* 从此映射中移除指定key的映射关系(如果存在)。
*
* @param key 指定key
* @return 返回以前与指定key关联的value;如果该键没有映射关系,则返回 null。
* @throws ClassCastException 如果指定key无法与Map中的结点的key进行比较
* @throws NullPointerException 如果指定key为 null
*/
public V remove(Object key) {
//内部调用doRemove方法,传递key 、null
return doRemove(key, null);
}
复制代码
3.5.2 remove(key, value)
public boolean remove(Object key, Object value)
如果此集合存在某个结点的key与指定key相等并且value与指定的value相等,那么从此集合中移除该结点。移除成功返回true,value为null或者移除失败返回false。
如果指定key无法与Map中的结点的key进行比较,则抛出ClassCastException,如果指定key为 null,则抛出NullPointerException。
/**
1. 如果此集合存在某个结点的key与指定key相等并且value与指定的value相等,那么从此Map中移除该结点。
2. 3. @throws ClassCastException 如果指定key无法与Map中的结点的key进行比较
4. @throws NullPointerException 如果指定key为 null
*/
public boolean remove(Object key, Object value) {
//key的检验
if (key == null)
throw new NullPointerException();
//如果value不等于null
//并且 调用doRemove方法(传递key、value)的返回值不为null,那么返回true
return value != null && doRemove(key, value) != null;
}
复制代码
3.5.3 doRemove删除结点
doRemove方法是删除数据的公用内部方法,也是提供了删除逻辑的主要代码实现。computeIfPresent、compute、merge、remove(k,v)等方法内部也调用了doRemove方法。大概步骤为:
- 对key进行null校验。
- 开启一个死循环:
- 初始化一些数据,q = head,为最新的head,r = q.right,为q的后继索引结点;
- 内部再开启一个死循环: 1. 如果n为null,说明b后面没有了结点,即整个跳表没有key和指定key相等的结点了。break outer直接跳出外层死循环,那么这里的内外死循环都彻底环结束,最后将返回null。 2. f=n.next,即f为n的后继;如果此时n不等于 3. b.next,因为ConcurrentSkipListMap没有锁,因此可能在这里的b的后继发生了改变,比如有另一个线程抢先一步在b后面插入了结点;break跳出内层循环,继续外层循环,相当于重新开始内层循环,重新查找b、n。 4. 如果n的value为null,说明n结点的数据被删除了,但是后续引用关系没有删除,n调用Node结点的helpDelete方法帮助删除数据结点,帮助成功之后,break跳出内层循环,继续外层循环,相当于重新开始内层循环,重新查找b、n。 5. 如果b的value为null,或者 v等于n,即n是一个标记结点说明b结点的数据被删除了,但是后续引用关系没有删除。break跳出内层循环,继续外层循环,相当于重新开始内层循环,重新查找b、n。 6. findPredecessor方法是查找小于指定key的具有索引关系的最大Node结点,这里继续调用cpr将指定key和n的key相比较获取结果c,如果c小于0,说明指定key小于n的key,那就是没有找到相同的key。break outer直接跳出外层死循环,那么这里的内外死循环都彻底环结束,最后将返回null; 7. 如果c大于0,,即指定key大于n的key,那么需要继续向后查找。改变b、n的引用,都向后移动一位,然后结束本次内层循环,继续下一次循环,直到找到真正的等于或者小于key的结点 或者 查找完毕也没找到。到这一步,说明 8. c等于0,说明指定key等于n的key,那么找到了key相等的结点。现在判断value,如果value不为null并且value不相等(equals),那么说明没有找到key和value都相等的结点;break outer直接跳出外层死循环,那么这里的内外死循环都彻底环结束,最后将返回nul 9. 到这里说明真正的找到了key或者key-value相等的结点,尝试CAS将带删除结点n的 value 置为 null。CAS失败则,break跳出内层循环,继续外层循环,相当于重新开始内层循环,重新查找b、n。 10. 如果CAS成功,那么调用尝试appendMarker 在n后面CAS的追加一个标记结点x n->f 更改为 n -> x -> f;如果CAS追加成功,那么尝试调用casNext 更改b数据结点的next引用关系,由 b -> n 更改为 b -> f。如果上面两个CAS操作有任意一个失败了那么调用findNode查找key同时尝试清理在遍历过程中所有遇到的 value 为 null 的结点。 11. 如果都CAS成功了,那么再调用一次findPredecessor用于清理索引链表中被删除数据结点关联的索引结点。如果head.right为null,即顶层索引链表只有一个头索引结点,那么调用tryReduceLevel尝试降低层级。 12. 返回vv,即被删除的结点的value,方法结束。
/**
* 主要删除逻辑:
* 定位结点,value置为null,附加删除标记结点,断开数据结点引用关系,断开索引结点引用关系,并可能降低最高索引层级
*
* @param key 指定key
* @param value 指定value,如果非空,则值必须也想等
* @return 被删除的结点,如果没有找到,则返回null
*/
final V doRemove(Object key, Object value) {
//key的校验
if (key == null)
throw new NullPointerException();
//获取比较器
Comparator<? super K> cmp = comparator;
/*开启死循环,尝试删除结点*/
outer:
for (; ; ) {
/*
* 调用findPredecessor查找小于指定key的具有索引关系的最大Node结点,没找到就返回数据链表头结点,返回值赋值为b;
* n=b.next,即后继
* 内部再开启一个死循环
*/
for (Node<K, V> b = findPredecessor(key, cmp), n = b.next; ; ) {
Object v;
int c;
//如果n为null,说明b后面没有了结点,即整个跳表没有key和指定key相等的结点了
if (n == null)
//break outer直接跳出外层死循环,那么这里的内外死循环都彻底环结束,最后将返回null
break outer;
//f=n.next,即f为n的后继
Node<K, V> f = n.next;
//如果此时n不等于b.next,因为ConcurrentSkipListMap没有锁,因此可能在这里的b的后继发生了改变,比如有另一个线程抢先一步在b后面插入了结点
if (n != b.next) // inconsistent read
//break跳出内层循环,继续外层循环,相当于重新开始内层循环,重新查找b、n
break;
//如果n的value为null,说明n结点的数据被删除了,但是后续引用关系没有删除
if ((v = n.value) == null) { // n is deleted
//n调用Node结点的helpDelete方法帮助删除数据结点
n.helpDelete(b, f);
//帮助成功之后,break跳出内层循环,继续外层循环,相当于重新开始内层循环,重新查找b、n
break;
}
/*
* 如果b的value为null,
* 或者 v等于n,即n是一个标记结点
* 说明b结点的数据被删除了,但是后续引用关系没有删除
*/
if (b.value == null || v == n) // b is deleted
//这里没法帮忙了
//break跳出内层循环,继续外层循环,相当于重新开始内层循环,重新查找b、n
break;
/*
* findPredecessor方法是查找小于指定key的具有索引关系的最大Node结点,
* 这里继续调用cpr将指定key和n的key相比较获取结果c
* 如果c小于0,说明指定key小于n的key,那就是没有找到相同的key
*/
if ((c = cpr(cmp, key, n.key)) < 0)
//break outer直接跳出外层死循环,那么这里的内外死循环都彻底环结束,最后将返回null
break outer;
//如果c大于0,,即指定key大于n的key,那么需要继续向后查找
if (c > 0) {
//改变b、n的引用,都向后移动一位,然后结束本次内层循环,继续下一次循环,
//直到找到真正的等于或者小于key的结点 或者 查找完毕也没找到。
b = n;
n = f;
continue;
}
//到这一步,说明c等于0,说明指定key等于n的key,那么找到了key相等的结点
//现在判断value,如果value不为null并且value不相等(equals),那么说明没有找到key和value都相等的结点
if (value != null && !value.equals(v))
//break outer直接跳出外层死循环,那么这里的内外死循环都彻底环结束,最后将返回null
break outer;
//到这里说明真正的找到了key或者key-value相等的结点
//尝试CAS将带删除结点n的 value 置为 null
if (!n.casValue(v, null))
//CAS失败则,break跳出内层循环,继续外层循环,相当于重新开始内层循环,重新查找b、n
break;
//如果CAS成功,那么调用尝试调用appendMarker 在n后面CAS的追加一个标记结点x n->f 更改为 n -> x -> f
//如果CAS追加成功,那么尝试调用casNext 更改b数据结点的next引用关系,由 b -> n 更改为 b -> f
/*如果上面两个CAS操作有任意一个失败了*/
if (!n.appendMarker(f) || !b.casNext(n, f))
//那么调用findNode查找key同时尝试清理在遍历过程中所有遇到的 value 为 null 的结点。
findNode(key); // retry via findNode
/*如果都成功了*/
else {
//那么再调用一次findPredecessor用于清理索引链表中被删除数据结点关联的索引结点
findPredecessor(key, cmp); // clean index
//如果head.right为null,即顶层索引链表只有一个头索引结点,那么尝试降低层级
if (head.right == null)
//调用tryReduceLevel尝试降低层级
tryReduceLevel();
}
//返回vv,即被删除的结点的value,方法结束
@SuppressWarnings("unchecked") V vv = (V) v;
return vv;
}
}
//返回null,方法结束
return null;
}
/**
* Node结点类中的方法
* CAS的将当前结点的value值从cmp更新为val
*
* @param cmp 预期原值
* @param val 新值
* @return true 成功 false 失败
*/
boolean casValue(Object cmp, Object val) {
return UNSAFE.compareAndSwapObject(this, valueOffset, cmp, val);
}
/**
* Node结点类中的方法
* CAS的将当前结点的next值从cmp更新为val
*
* @param cmp 预期后期原值
* @param val 新值
* @return true 成功 false 失败
*/
boolean casNext(Node<K, V> cmp, Node<K, V> val) {
return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}
/**
1. Node结点类中的方法
2. CAS的将当前结点的next值从 f 更新为 next指向f的一个标记结点
3. 4. @param f 预期后期原值
5. @return true 成功 false 失败
*/
boolean appendMarker(Node<K, V> f) {
//标记结点就是value指向自己的结点,key为null,next为f,用于标记其前驱被删除
return casNext(f, new Node<K, V>(f));
}
复制代码
总结起来大概步骤就是:
- 定位结点,失败则返回null,方法结束成功则继续,下面开始删除的逻辑:
- 第一步:结点的value置为null,失败则循环重试;
- 第二步:被删除结点n之后追加删除标记结点x,n->f 更改为 n -> x -> f;失败则调用findNode清除被遇到的删除的结点(重试一次);
- 第三步:CAS追加标记结点成功之后,断开被删除结点n和其“前驱”b的引用关系,b -> n 更改为 b -> f;失败则调用findNode清除被遇到的删除的结点(重试一次);
- 第四步:如果上面的两个CAS都成功,那么再调用一次findPredecessor用于清理索引链表中被删除数据结点关联的索引结点;
- 之后如果head.right==null,那么调用tryReduceLevel尝试降低索引层级。
- 最终返回被删除的value。
可以看到,这里的“删除”和平时的删除不一样,有可能仅仅是将value置为null,但是后续步骤没有CAS成功(没有移除相关结点之间的引用关系)就返回了!这种情况被称为“懒删除”,剩下的步骤它自己会在findNode方法中可能会走一步(调用内部的helpDelete方法,helpDelete方法只会允许一条线程帮助第二步或者第三步)。
而其它的步骤,比如第二步和第三步,只有等到其他操作的线程在遍历到底层数据链表的时候判断(value=null)时才会调用helpDelete帮助删除,而第四步只有等到其他操作的线程在调用findPredecessor方法并判断(value=null)时才会帮助删除索引链表中被删除数据结点关联的索引结点;
这里的删除操作的第二、三、四步不采用循环的CAS,主要是为了性能考虑,防止高并发情况下大量的线程空转而造成CPU占用过高。实际上在统计结点数量以及遍历跳表等操作的时候,会跳过value为null的结点,因此除了value=null之外的步骤都不是在一次删除过程中必须要完成的。在ConcurrentSkipListMap中,对于结点的非必须成功的CAS操作都没有使用自旋,而是仅仅尝试一次,能成就成不能成那等到后面其他线程再做,这样可以让更多的线程将更多时间用在关键的地方(比如对于结点x,有一些关键的CAS操作和非关键的CAS操作,如果非关键的CAS操作也是用循环的方式,那么可能造成关键操作一直不成功,这样影响了性能),即使非关键操作一直没有成功,那也不过只会增加一些内存空间的占用,但是能够提升效率。
另外这里删除操作的一系列设置标记结点-改变引用的那个步骤,主要是为了能够更好的与插入、修改、读取操作并发,这里我们已经在前面的put中见识到了对于删除过程的帮助处理。
3.5.3.1.1 tryReduceLevel尝试降低索引层级
/**
* 尝试降低层级
* 只有在最上面的三层索引链表看起来是空的时候,才会尝试减少一层,即CAS的尝试将head指向低一层的索引链表头结点
* 如果在移除最顶层之后发现最顶层又有索引结点了(其他线程放进去的),那么又会尝试CAS的将head改为原来的顶层索引链表头结点
*/
private void tryReduceLevel() {
//获取此时的head
HeadIndex<K,V> h = head;
HeadIndex<K,V> d;
HeadIndex<K,V> e;
//如果此时最大层级大于3
if (h.level > 3 &&
//d = h.down 并且 d不为null
(d = (HeadIndex<K,V>)h.down) != null &&
//e = d.down 并且 e不为null
(e = (HeadIndex<K,V>)d.down) != null &&
//并且e.right == null
e.right == null &&
//并且d.right == null
d.right == null &&
//并且h.right == null
h.right == null &&
//尝试CAS的将head的指向从h变成d 并且成功
casHead(h, d) && // try to set
//成功之后又发现 h.right != null,说明出现了并发操作,其他线程在最顶层存放了索引结点
h.right != null) // recheck
//尝试CAS的将head的指向从d变成h
casHead(d, h); // try to backout
}
复制代码
3.5.4 执行流程
假设某个跳跃表的结构如下:
现在我们要删除key=9的键值对!
首先我们需要定位到key=9的结点n,然后尝试CAS的将n.value设置为null,假设成功:
然后会调用尝试调用appendMarker 在n后面CAS的追加一个标记结点x,n的next指向x,而x的value指向自己,key为null,next指向f,假设成功:
然后调用casNext 更改b数据结点的next引用关系,由 b -> n 更改为 b -> f,假设成功:
可以看到,此时仅仅通过底层数据链表已经无法通过next访问到n和x结点了,但是n结点还存在上面对应的索引结点,最终还是关联到了head引用,还不会被GC清理。因此下一步是调用findPredecessor用于清理索引链表中被删除数据结点关联的索引结点。
在findPredecessor方法中会遍历到r=n,然后会发现n.value=null,因此会调用q.unlink(r),移除在某层索引的right横向引用关系,unlink方法源码前面讲过,很简单的将当前结点的next改为r.next,首先是最顶层:
调用q.unlink(r)之后:
可以看到,最顶层的索引链表与对应索引结点的横向关系已经没有了。然后在findPredecessor方法中会continue,继续下一次循环,此时会移动到下一层链表:
调用q.unlink(r)之后:
可以看到,最顶层的索引链表与对应索引结点的横向关系已经没有了。然后在findPredecessor方法中会continue,继续下一次循环,下面没有索引链表了,最终会返回q.node。
到此与被删除数据结点n相关关联的索引结点已被移除对应的索引链表。可能会有疑问,明明还有索引关系,怎么还会算作被清理了呢?实际上,上面的结构换一种图示之后:
现代Java虚拟机采用可达性分析算法来分析垃圾,实际上被移除的引用关系的那些节点。虽然还有引用,但是既不能作为GC root,也不能通过其他GC root可达,所以它们指向的对象将会被判定为是可回收的对象,在下一次GC中将会被回收。最终的结构如下:
以上只是在没有线程竞争以及没有遇到无效结点的情况,在高并发环境下的删除过程将会更加复杂。那可真是让人头大啊!
3.6 get操作
public V get(Object key)
返回指定key所映射到的值;如果此映射不包含该key的映射关系,则返回 null。
如果指定键无法与映射中的当前键进行比较,那么抛出ClassCastException;如果指定键为 null,那么抛出NullPointerException。
/**
* 返回指定key所映射到的值
*
* @param key 指定key
* @return 返回指定key所映射到的值;如果此映射不包含该key的映射关系,则返回 null。
* @throws ClassCastException 如果指定键无法与映射中的当前键进行比较
* @throws NullPointerException 如果指定键为 null
*/
public V get(Object key) {
//内部调用doGet方法,传入key
return doGet(key);
}
复制代码
3.6.1.1 doGet获取指定key的value
doGet方法是获取数据的公用内部方法,也是提供了获取逻辑的主要代码实现。containsKey、getOrDefault、computeIfAbsent等方法内部也调用了doGet方法。
源码非常简单,就是首先调用findPredecessor查找小于指定key的具有索引关系的最大Node结点,然后从该结点开始在数据链表中向后查找等于指定key的结点,找到就返回结点的value,找不到就返回null。
与findNode方法几乎相同,但是返回找到的值,因此不再讲解!
/**
* 获取键的值。与findNode方法几乎相同,但是返回找到的值
*
* @param key 指定key
* @return key对应的value;如果没有,则返回null
*/
private V doGet(Object key) {
//key检验
if (key == null)
throw new NullPointerException();
//cmp保存全局的比较器,可能为null
Comparator<? super K> cmp = comparator;
/*开启一个死循环,尝试查找结点*/
outer:
for (; ; ) {
/*
* 调用findPredecessor查找小于指定key的具有索引关系的最大Node结点,没找到就返回数据链表头结点,返回值赋值为b;n=b.next,即后继
* 内部再开启一个死循环
*/
for (Node<K, V> b = findPredecessor(key, cmp), n = b.next; ; ) {
Object v;
int c;
//如果n为null,说明b后面没有了结点,即整个跳表没有key和指定key相等的结点了
if (n == null)
//break outer直接跳出外层死循环,那么这里的内外死循环都彻底环结束,最后将返回null
break outer;
//f=n.next,即f为n的后继
Node<K, V> f = n.next;
//如果此时n不等于b.next,因为ConcurrentSkipListMap没有锁,因此可能在这里的b的后继发生了改变,比如有另一个线程抢先一步在b后面插入了结点
if (n != b.next) // inconsistent read
//break跳出内层循环,继续外层循环,相当于重新开始内层循环,重新查找b、n
break;
//如果n的value为null,说明n结点的数据被删除了,但是后续引用关系没有删除
if ((v = n.value) == null) { // n is deleted
//n调用Node结点的helpDelete方法帮助删除数据结点
n.helpDelete(b, f);
//帮助成功之后,break跳出内层循环,继续外层循环,相当于重新开始内层循环,重新查找b、n
break;
}
/*
* 如果b的value为null,
* 或者 v等于n,即n是一个标记结点
* 说明b结点的数据被删除了,但是后续引用关系没有删除
*/
if (b.value == null || v == n) // b is deleted
//这里没法帮忙了
//break跳出内层循环,继续外层循环,相当于重新开始内层循环,重新查找b、n
break;
/*
* findPredecessor方法是查找小于指定key的具有索引关系的最大Node结点,
* 这里继续调用cpr将指定key和n的key相比较获取结果c,
* 如果c等于0,说明指定key等于n的key,那么找到了
*/
if ((c = cpr(cmp, key, n.key)) == 0) {
//直接返回v,方法结束
@SuppressWarnings("unchecked") V vv = (V) v;
return vv;
}
//如果c小于0,说明指定key小于n的key
//那就是没有找到相同的key
if (c < 0)
//break outer直接跳出外层死循环,那么这里的内外死循环都彻底环结束,最后将返回null
break outer;
//到这里说明c大于0,即指定key大于n的key,那么需要继续向后查找
//改变b、n的引用,都向后移动一位,然后结束本次内层循环,继续下一次循环,
//直到找到真正的等于或者小于key的结点 或者 查找完毕也没找到。
b = n;
n = f;
}
}
//返回null
return null;
}
复制代码
3.7 reeplace操作
3.7.1 replace(key, value)
public V replace(K key, V value)
如果指定key对应的结点存在,那么使用指定value替换旧value。返回以前与指定键关联的值;如果没有该键的映射关系,则返回 null。
如果指定key无法与Map中的结点的key进行比较,则抛出ClassCastException,如果任何参数为null,则抛出NullPointerException。
/**
* 如果指定key对应的结点存在,那么使用指定value替换旧value。
*
* @return 返回以前与指定键关联的值;如果没有该键的映射关系,则返回 null。
* @throws ClassCastException 如果指定key无法与Map中的结点的key进行比较
* @throws NullPointerException 如果指定key为 null
*/
public V replace(K key, V value) {
//如果key或者value为null,那么抛出NullPointerException
if (key == null || value == null)
throw new NullPointerException();
/*死循环*/
for (; ; ) {
Node<K, V> n;
Object v;
//调用findNode查找key相等的结点,这个方法前面讲过了
if ((n = findNode(key)) == null)
//如果返回的n为null,那么返回null
return null;
/*到这里说明找到了key相等的结点n*/
//如果n的value不等于null
//那么尝试CAS的将n的value 从v设置为value
if ((v = n.value) != null && n.casValue(v, value)) {
@SuppressWarnings("unchecked") V vv = (V) v;
//如果上面两边的表达式都返回true,那么说明替换成功,返回旧的value
return vv;
}
//n.value为null或者CAS失败,那么循环重试
}
}
复制代码
3.7.2 replace(key, oldValue, newValue)
public boolean replace(K key, V oldValue, V newValue)
如果指定key-value对应的结点存在,那么使用newValue替换value。如果该值被替换,则返回 true。
如果指定key无法与Map中的结点的key进行比较,则抛出ClassCastException,如果任何参数为null,则抛出NullPointerException。
/**
* 如果指定key-value对应的结点存在,那么使用newValue替换value。
*
* @param key 指定key
* @param oldValue 指定value
* @param newValue 要替换的行value
* @return 如果该值被替换,则返回 true。
* @throws ClassCastException 如果指定key无法与Map中的结点的key进行比较
* @throws NullPointerException 如果指定key为 null
*/
public boolean replace(K key, V oldValue, V newValue) {
//如果任意参数为null,那么抛出NullPointerException
if (key == null || oldValue == null || newValue == null)
throw new NullPointerException();
/*死循环*/
for (; ; ) {
Node<K, V> n;
Object v;
//调用findNode查找key相等的结点,这个方法前面讲过了,
if ((n = findNode(key)) == null)
//如果返回的n为null,那么返回false
return false;
//如果n的value不等于null
if ((v = n.value) != null) {
//如果指定value不等于n的value
if (!oldValue.equals(v))
//那么返回false
return false;
//如果key和value都相等,那么尝试CAS的将n的value 从v设置为value
if (n.casValue(v, newValue))
//CAS成功则返回true
return true;
}
//n.value为null或者CAS失败,那么循环重试
}
}
复制代码
3.8 contains操作
3.8.1 containsKey
public boolean containsKey(Object key)
如果此集合存在某个结点的key与指定key相等,那么返回true。
如果指定key无法与Map中的结点的key进行比较,则抛出ClassCastException,如果指定key为 null,则抛出NullPointerException。
/**
* 如果此集合包含指定key的映射关系,则返回 true。
*
* @param key 指定key
* @return 如果此集合包含指定key的映射关系,则返回 true。
* @throws ClassCastException 如果指定key无法与Map中的结点的key进行比较
* @throws NullPointerException 如果指定key为 null
*/
public boolean containsKey(Object key) {
//很简单,内部调用doGet,如果返回值不为null,说明存在key相等的结点,那么返回true
return doGet(key) != null;
}
复制代码
3.8.2 containsValue
public boolean containsValue(Object value)
如果此集合存在某个结点的value与指定value相等,那么返回true。如果指定value为 null,则抛出NullPointerException。
可以看到这个方法比较的是value,因此不能使用索引,那么只能从头开始遍历底层链表的数据结点,每一个结点都比较一次value,直到最后一个有效的结点。这样相当于线性时间复杂度O(n),因此效率很低。
/**
* 如果此集合存在某个结点的value与指定value相等,那么返回true
*
* @param value 指定value
* @return 如果此集合存在某个结点的value与指定value相等,那么返回true。
* @throws NullPointerException 如果指定value为 null
*/
public boolean containsValue(Object value) {
//value的校验
if (value == null)
throw new NullPointerException();
//调用findFirst获取第一个真正有效的数据结点n,没找到就返回null
//如果n不等于null那么继续下依次循环,循环一次n向右移动:n = n.next
for (Node<K, V> n = findFirst(); n != null; n = n.next) {
//获取有效的value:如果当前结点不是数据链表头结点(哨兵结点) 并且不是标记结点,那么返回value,否则返回null
V v = n.getValidValue();
//如果v不等于null并且value等于指定的v(通过equals比较)
if (v != null && value.equals(v))
//那么返回true
return true;
}
//最终返回false
return false;
}
/**
* 获取第一个真正有效的数据结点
*
* @return 第一个真正有效的数据结点,没找到就返回null
*/
final Node<K, V> findFirst() {
//死循环
for (Node<K, V> b, n; ; ) {
//如果head的next为null,那么返回null,说明没有有效的数据结点
if ((n = (b = head.node).next) == null)
return null;
//如果n.value不为null
if (n.value != null)
//那么返回n
return n;
//否则,调用helpDelete帮助删除该结点,然后继续下一次循环,直到找到value不为null的第一个结点
n.helpDelete(b, n.next);
}
}
/**
* Node结点类的方法
*
* @return 如果当前结点不是数据链表头结点(哨兵结点) 并且不是标记结点,那么返回value,否则返回null
*/
V getValidValue() {
//获取value
Object v = value;
//如果value等于自己,说明是标记结点 或者 如果v等于BASE_HEADER,说明是数据链表头结点(哨兵结点)
if (v == this || v == BASE_HEADER)
//那么返回null
return null;
@SuppressWarnings("unchecked") V vv = (V) v;
//否则返回
return vv;
}
复制代码
3.9 size操作
和其他的JUC中的并发集合一样,计数的方法都是不准确的。
3.9.1 size
public int size()
返回此集合中的有效的键值对数量。如果数量大于 Integer.MAX_VALUE,则返回 Integer.MAX_VALUE。
/**
* 返回此集合中的有效的键值对数量。如果数量大于 Integer.MAX_VALUE,则返回 Integer.MAX_VALUE。和其他的JUC中的并发集合一样,计数的方法都是不准确的。
*
* @return 集合的键值对数量
*/
public int size() {
//初始化计数器为0
long count = 0;
//调用findFirst获取去第一个有效结点
for (Node<K, V> n = findFirst(); n != null; n = n.next) {
//如果有效的value不等于null,说明该结点是有效的结点
//这里就把那些删除到一半键值对排除了
if (n.getValidValue() != null)
//计数器自增1
++count;
}
//如果count超过了Integer.MAX_VALUE,那么就返回Integer.MAX_VALUE
return (count >= Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int) count;
}
复制代码
3.9.2 isEmpty
public boolean isEmpty()
如果此映射未包含有效的键值对,则返回 true。
/**
* @return 如果此映射未包含有效的键值对,则返回 true。
*/
public boolean isEmpty() {
//很简单,调用findFirst获取第一个有效的结点,如果为null说明该集合就没有任何有效的键值对
return findFirst() == null;
}
复制代码
3.10 导航操作
排序的集合都拥有一系列的导航的方法,这样的方法在所有提供的方法中占有很大的比重。
比如lowerEntry、floorEntry、ceilingEntry 和 higherEntry 分别返回与小于、小于等于、大于等于、大于给定键的键关联的 Map.Entry 对象,如果不存在这样的键,则返回 null。类似地,方法 lowerKey、floorKey、ceilingKey 和 higherKey 只返回关联的键。
firstEntry、pollFirstEntry、lastEntry 和 pollLastEntry 方法,它们返回和/或移除最小和最大的映射关系(如果存在),否则返回 null。
subMap、headMap 和 tailMap分别用于返回小于(或等于)指定key、位于fromKey(或等于)- toKey(或等于)之间、大于(或等于)指定key的一批数据结点。
等等,还有很多方法,所有这些方法是为查找条目而不是遍历条目而设计的。因此ConcurrentSkipListMap和TreeMap一样都是更多的被用来查找数据。
下面以lowerEntry方法为例子讲解,因为很多类似方法都是调用同一个内部方法来实现的!
3.10.1 lowerEntry
Map.Entry<K,V> lowerEntry(K key)
返回一个Entry结点,它是小于指定key的最大结点;如果不存在这样的结点,则返回 null。
如果指定key无法与Map中的结点的key进行比较,则抛出ClassCastException,如果指定key为 null,则抛出NullPointerException。
/**
* 返回一个Entry结点,它是小于指定key的最大结点;如果不存在这样的结点,则返回 null。
*
* @throws ClassCastException {@inheritDoc}
* @throws NullPointerException if the specified key is null
*/
public Map.Entry<K, V> lowerEntry(K key) {
//内部调用getNear,传入LT
return getNear(key, LT);
}
/**
* 返回基于数据结点的简单结点,或者null
*
* @param key 指定key
* @param rel 关系标记,EQ, LT, GT,或者它们的组合
* @return 基于数据结点的简单结点,如果没有找到数据结点则返回null
*/
final AbstractMap.SimpleImmutableEntry<K, V> getNear(K key, int rel) {
Comparator<? super K> cmp = comparator;
for (; ; ) {
//调用findNear方法,传入key、rel、cmp
//findNear用于查找和指定key具有指定关系rel的数据结点,返回符合关系的结点,没找到则返回null
Node<K, V> n = findNear(key, rel, cmp);
//如果n为null
if (n == null)
//说明没找到,那么返回null
return null;
//找到了,就将Node数据节点包装成一个简单结点然后返回
AbstractMap.SimpleImmutableEntry<K, V> e = n.createSnapshot();
if (e != null)
return e;
}
}
// 一批关系标记常量,可以单独作为参数传递也可以组合起来传递 比如EQ|GT
/**
* 相等,二进制为 01
*/
private static final int EQ = 1;
/**
* 小于,二进制为 10
*/
private static final int LT = 2;
/**
* 大于,二进制为 0
* 实际检查大于是通过 非LT 来判断的
*/
private static final int GT = 0; // Actually checked as !LT
/**
* 导航方法的用于查找和指定key具有指定关系rel的数据结点的通用内部方法
* 该方法中判断结点关系使用的是位运算,效率非常高,但是涉及到一些位运算常识可能新手不易理解
*
* @param key 指定key
* @param rel 关系标记,EQ, LT, GT,或者它们的组合
* @return 返回符合关系的结点,没找到则返回null
*/
final Node<K, V> findNear(K key, int rel, Comparator<? super K> cmp) {
//key的校验
if (key == null)
throw new NullPointerException();
/*开启一个死循环*/
for (; ; ) {
/*
* 调用findPredecessor查找小于指定key的具有索引关系的最大Node结点,没找到就返回数据链表头结点,返回值赋值为b;n=b.next,即后继
* 内部再开启一个死循环
*/
for (Node<K, V> b = findPredecessor(key, cmp), n = b.next; ; ) {
Object v;
//如果n为null,说明b后面没有了结点,即整个跳表没有key会 大于等于 指定key了
if (n == null)
//如果rel & LT等于0,那么rel肯定没有包含小于(LT)的关系
//或者rel & LT不等于0,但是b.isBaseHeader()为true,即此节点是数据链表头结点
//以上两种情况满足一种,那么返回null,因为没找到符合关系的结点,否则返回b,b就是符合关系的结点,因为此时b是最后一个数据结点
return ((rel & LT) == 0 || b.isBaseHeader()) ? null : b;
//f=n.next,即f为n的后继
Node<K, V> f = n.next;
//如果此时n不等于b.next,因为ConcurrentSkipListMap没有锁,因此可能在这里的b的后继发生了改变,比如有另一个线程抢先一步在b后面插入了结点
if (n != b.next) // inconsistent read
//如果此时n不等于b.next,因为ConcurrentSkipListMap没有锁,因此可能在这里的b的后继发生了改变,比如有另一个线程抢先一步在b后面插入了结点
break;
//如果n的value为null,说明n结点的数据被删除了,但是后续引用关系没有删除
if ((v = n.value) == null) { // n is deleted
//n调用Node结点的helpDelete方法帮助删除数据结点
n.helpDelete(b, f);
//帮助成功之后,break跳出内层循环,继续外层循环,相当于重新开始内层循环,重新查找b、n
break;
}
/*
* 如果b的value为null,
* 或者 v等于n,即n是一个标记结点
* 说明b结点的数据被删除了,但是后续引用关系没有删除
*/
if (b.value == null || v == n) // b is deleted
//这里没法帮忙了
//break跳出内层循环,继续外层循环,相当于重新开始内层循环,重新查找b、n
break;
/*
* findPredecessor方法是查找小于指定key的具有索引关系的最大Node结点,
* 这里继续调用cpr将指定key和n的key相比较获取结果c,
*
*/
int c = cpr(cmp, key, n.key);
//如果c等于0,说明指定key等于n的key,这是相等的关系 并且 如果 (rel & EQ) 不等于0,那么rel肯定包含等于(EQ)的关系,那么可以返回n
if ((c == 0 && (rel & EQ) != 0) ||
//否则,如果c小于0,说明指定key小于n的key 并且(rel & LT) 等于0,那么rel肯定没有包含小于(LT)的关系,那就是大于的关系,那么也可以返回n
(c < 0 && (rel & LT) == 0))
return n;
//如果c小于等于0 并且rel & LT不等于0,那么rel肯定包含小于(LT)的关系,
if (c <= 0 && (rel & LT) != 0)
//调用isBaseHeader判断b是不是数据链表头结点
//如果是那么返回null,不是则返回b
return b.isBaseHeader() ? null : b;
//到这里说明c大于0,即指定key大于n的key,那么需要继续向后查找
//改变b、n的引用,都向后移动一位,然后结束本次内层循环,继续下一次循环,
//直到找到真正的等于或者小于key的结点 或者 查找完毕也没找到。
b = n;
n = f;
}
}
}
/**
* Node结点类中的方法
*
* @return 如果此节点是数据链表头结点,则返回true
*/
boolean isBaseHeader() {
return value == BASE_HEADER;
}
/**
* Node结点类中的方法
*
* @return 如果此结点是有效结点,那么根据该结点的key和value构建一个简单结点并返回,否则返回null
*/
AbstractMap.SimpleImmutableEntry<K, V> createSnapshot() {
Object v = value;
//如果 v==null,说明该结点被删除
//或者 如果v==this,说明该结点是标记结点
//或者 如果v==BASE_HEADER,说明该结点是数据链表的头结点,无意义
if (v == null || v == this || v == BASE_HEADER)
//以上三种情况满足一种即返回null
return null;
@SuppressWarnings("unchecked") V vv = (V) v;
//否则,返回一个AbstractMap的SimpleImmutableEntry内部类实例,作为一个简单节点,传递key、value
return new AbstractMap.SimpleImmutableEntry<K, V>(key, vv);
}
/**
* AbstractMap中的内部类
* 不可变的简单数据结点,仅仅维护了某个结点在某个时刻的key和value的快照
* 不支持setValue改变值的方法,强行调用将抛出UnsupportedOperationException异常
*
* @since 1.6
*/
public static class SimpleImmutableEntry<K, V>
implements Map.Entry<K, V>, java.io.Serializable {
private static final long serialVersionUID = 7138329143949025153L;
/**
* key快照
*/
private final K key;
/**
* value快照
*/
private final V value;
/**
* 创建简单的不可变的数据结点
*
* @param key 来自于数据结点的key
* @param value 来自于数据结点的value
*/
public SimpleImmutableEntry(K key, V value) {
this.key = key;
this.value = value;
}
/**
* 根据Entry结点创建一个不可变的简单数据结点
*
* @param entry the entry to copy
*/
public SimpleImmutableEntry(Map.Entry<? extends K, ? extends V> entry) {
this.key = entry.getKey();
this.value = entry.getValue();
}
/**
* @return 返回key快照
*/
public K getKey() {
return key;
}
/**
* @return 返回value快照
*/
public V getValue() {
return value;
}
/**
* 替换value的操作,在该类的实现中不被支持,强行调用将抛出UnsupportedOperationException异常
* 因为该类维护的仅仅是一个快照,是不可变,任何修改都没有意义
*
* @param value 新value
* @return (Does not return)
* @throws UnsupportedOperationException 任何调用操作
*/
public V setValue(V value) {
throw new UnsupportedOperationException();
}
/**
* 比较的方法,首先判断类型,类型一致之后相当于如下操作:
* if<pre>
* (e1.getKey()==null ?
* e2.getKey()==null :
* e1.getKey().equals(e2.getKey()))
* &&
* (e1.getValue()==null ?
* e2.getValue()==null :
* e1.getValue().equals(e2.getValue()))</pre>
*
* @param o object to be compared for equality with this map entry
* @return {@code true} if the specified object is equal to this map
* entry
* @see #hashCode
*/
public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?, ?> e = (Map.Entry<?, ?>) o;
return eq(key, e.getKey()) && eq(value, e.getValue());
}
/**
* @return 返回hashcode
*/
public int hashCode() {
return (key == null ? 0 : key.hashCode()) ^
(value == null ? 0 : value.hashCode());
}
/**
* @return 格式化输出
*/
public String toString() {
return key + "=" + value;
}
}
复制代码
可以发现,内部判断结点的关系使用的是位运算,效率非常高。同时返回的节点是在AbstractMap中定义的SimpleImmutableEntry内部结点类,这个结点类是一种不可变的简单数据结点,仅仅维护了某个结点在某个时刻的key和value的快照,仅仅用于访问操作,不支持修改!
4 ConcurrentSkipListMap的总结
ConcurrentSkipListMap是JDK1.6的时候添加的一个支持排序Map集合类,相当于TreeMap的线程安全的版本,但是它不仅仅关注线程安全,它更关注的是并发效率。
ConcurrentSkipListMap内部采用CAS无锁算法+volatile来实现增删改查的线程安全,没有使用锁,因此任何操作都不会阻塞,任何操作都可以并发,因此效率非常高。带来的问题是如果线程竞争非常激烈,可能会因为大量线程持续空转而占用过多的CPU! 为什么使用跳跃表来实现安全并发的排序Map而不利用现成的TreeMap(红黑树)来实现呢?
因为跳跃表和红黑树的时间性能差别大不,最直接的答案就是跳跃表的实现比较简单,如果你以前没有了解过数据结构,那么你直接看这篇文章也有可能看懂跳跃表的实现原理,实际上跳跃表就是最底层一张数据链表和上面几层的索引链表。插入结点的时候,到底要不要增加索引结点或者层级完全随机的,虽然少量样本可能导致结构不完美,但是大量数据情况下,跳跃表的结构会更加均衡!删除结点的时候,则更加简单,删除数据结点以及关联的索引结点即可,并且删除的步骤都不是强关联的,后面的步骤没有完成也没关系,可以由其他线程帮助完成!
但是如果你以前没有了解过排序二叉树、AVL树等等基础的树形结构就直接去看TreeMap的实现,那你很可能会非常吃力。因为红黑树的实现复杂度很高,插入、删除之后需要根据不同的情况调衡平衡,这其中包括旋转(单旋转、双旋转)、变色、递归等操作。还有一个重要的理由就是,红黑树的实现中,由于结点操作的关联性非常强,导致一大片的代码往往需要保证连续(原子)操作,通常需要锁定一大片代码,因此如果仅仅需要线程安全那确实好做到,但是想要优化成为既安全又高效的并发集合,那非常的困难。最终Doug Lea选择跳跃表来实现安全高效的排序Map,或许以后会有即安全又高效的基于红黑树的排序Map实现吧!
那么跳跃表相比于红黑树有没有什么缺点呢?当然有,显而易见的就是跳跃表由于具有“索引”结点,因此需要占用更多的内存空间。即使到如今,数据结构的时间和空间效率往往不能兼得,跳跃表就是以空间换时间经典实现!不过目前的发展趋势似乎是对于时间性能的需求大于空间性能!另外基于随机数算法,如果数据量不是很大那么性能不是很稳定,并且这个随机数算法生成的是“伪随机数”!
ConcurrentSkipListMap即使采用比较简单的跳跃表结构,在JDK1.8中仍然具有三千多行代码,十五个内部类,仅次于ConcurrenthashMap(超过六千行代码,超过五十个内部类)。本文仅仅分析了ConcurrentSkipListMap的部分代码,并且可能分析的有些问题,如果有大神发现了,还请指出!
相关文章:
- 红黑树:数据结构—两万字的红黑树(RedBlackTree)的实现原理以及Java代码的完全实现。
- HashMap:Java集合—四万字的HashMap的源码深度解析与应用。
- ConcurrentHashMap:JUC—三万字的ConcurrentHashMap源码深度解析。
如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!