简介
跳表是一个随机化的数据结构,实质就是一种可以进行二分查找的有序链表。
跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。
跳表不仅能提高搜索性能,同时也可以提高插入和删除操作的性能。
存储结构
跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。
源码分析
主要内部类
内部类跟存储结构结合着来看,大概能预测到代码的组织方式。
// 数据节点,典型的单链表结构
static final class Node<K,V> {
final K key;
// 注意:这里value的类型是Object,而不是V
// 在删除元素的时候value会指向当前元素本身
volatile Object value;
volatile Node<K,V> next;
Node(K key, Object value, Node<K,V> next) {
this.key = key;
this.value = value;
this.next = next;
}
Node(Node<K,V> next) {
this.key = null;
this.value = this; // 当前元素本身(marker)
this.next = next;
}
}
// 索引节点,存储着对应的node值,及向下和向右的索引指针
static class Index<K,V> {
final Node<K,V> node;
final Index<K,V> down;
volatile Index<K,V> right;
Index(Node<K,V> node, Index<K,V> down, Index<K,V> right) {
this.node = node;
this.down = down;
this.right = right;
}
}
// 头索引节点,继承自Index,并扩展一个level字段,用于记录索引的层级
static final class HeadIndex<K,V> extends Index<K,V> {
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;
}
}
(1)Node,数据节点,存储数据的节点,典型的单链表结构;
(2)Index,索引节点,存储着对应的node值,及向下和向右的索引指针;
(3)HeadIndex,头索引节点,继承自Index,并扩展一个level字段,用于记录索引的层级;
构造方法
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);
}
四个构造方法里面都调用了initialize()这个方法,那么,这个方法里面有什么呢?
private static final Object BASE_HEADER = new Object();
private void initialize() {
keySet = null;
entrySet = null;
values = null;
descendingMap = null;
// Node(K key, Object value, Node<K,V> next)
// HeadIndex(Node<K,V> node, Index<K,V> down, Index<K,V> right, int level)
head = new HeadIndex<K,V>(new Node<K,V>(null, BASE_HEADER, null),
null, null, 1);
}
可以看到,这里初始化了一些属性,并创建了一个头索引节点,里面存储着一个数据节点,这个数据节点的值是空对象,且它的层级是1。
所以,初始化的时候,跳表中只有一个头索引节点,层级是1,数据节点是一个空对象,down和right都是null。
通过内部类的结构我们知道,一个头索引指针包含node, down, right三个指针,为了便于理解,我们把指向node的指针用虚线表示,其它两个用实线表示,也就是虚线不是表明方向的。
添加元素
我们知道跳表插入元素的时候会通过抛硬币的方式决定出它需要的层级,然后找到各层链中它所在的位置,最后通过单链表插入的方式把节点及索引插入进去来实现的。
那么,ConcurrentSkipList中是这么做的吗?让我们一起来探个究竟:
public V put(K key, V value) {
// 不能存储value为null的元素
// 因为value为null标记该元素被删除(后面会看到)
if (value == null)
throw new NullPointerException();
// 调用doPut()方法添加元素
return doPut(key, value, false);
}
private V doPut(K key, V value, boolean onlyIfAbsent) {
// 添加元素后存储在z中
Node<K,V> z; // added node
// key也不能为null
if (key == null)
throw new NullPointerException();
Comparator<? super K> cmp = comparator;
// Part I:找到目标节点的位置并插入
// 这里的目标节点是数据节点,也就是最底层的那条链
// 自旋
outer: for (;;) {
// 寻找目标节点之前最近的一个索引对应的数据节点,存储在b中,b=before
// 并把b的下一个数据节点存储在n中,n=next
// 为了便于描述,我这里把b叫做当前节点,n叫做下一个节点
for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
// 如果下一个节点不为空
// 就拿其key与目标节点的key比较,找到目标节点应该插入的位置
if (n != null) {
// v=value,存储节点value值
// c=compare,存储两个节点比较的大小
Object v; int c;
// n的下一个数据节点,也就是b的下一个节点的下一个节点(孙子节点)
Node<K,V> f = n.next;
// 如果n不为b的下一个节点
// 说明有其它线程修改了数据,则跳出内层循环
// 也就是回到了外层循环自旋的位置,从头来过
if (n != b.next) // inconsistent read
break;
// 如果n的value值为空,说明该节点已删除,协助删除节点
if ((v = n.value) == null) { // n is deleted
// todo 这里为啥会协助删除?后面讲
n.helpDelete(b, f);
break;
}
// 如果b的值为空或者v等于n,说明b已被删除
// 这时候n就是marker节点,那b就是被删除的那个
if (b.value == null || v == n) // b is deleted