ConcurrentSkipListMap其实是TreeMap的并发版本。TreeMap使用的是红黑树,并且按照key的顺序排序(自然顺序、自定义顺序),但是他是非线程安全的,如果在并发环境下,建议使用ConcurrentHashMap或者ConcurrentSkipListMap。
ConcurrentSkipListSet其实是TreeSet的并发版本。TreeSet底层使用红黑树,并且按照key的顺序排序(自然顺序、自定义顺序),但是他是非线程安全的,如果在并发环境下ConcurrentSkipListSet。
ConcurrentSkipListMap和ConcurrentSkipListSet底层使用跳表数据结构来实现,跳表全称叫做跳跃表,它是一个随机化的数据结构,可以被看做二叉树的一个变种,通过使用“跳跃式”查找的方式使得插入、读取数据时复杂度变成了O(logn)。它在性能上和红黑树,AVL树不相上下,但是跳表的原理非常简单,目前在Redis和LeveIDB中都有用到。
跳跃表是一种典型的“空间来换取时间”的一个算法,通过在每个节点中增加了向前的指针,从而提升查找的效率。实现跳表的基础是保证元素有序。
结构图
从图中可以看到, 跳跃表主要由以下部分构成:
- 表头(head):负责维护跳跃表的节点指针。
- 跳跃表节点:保存着元素值,以及多个层。
- 层:保存着指向其他元素的指针。高层的指针越过的元素数量大于等于低层的指针,为了提高查找的效率,程序总是从高层先开始访问,然后随着元素值范围的缩小,慢慢降低层次。
- 表尾:全部由 NULL 组成,表示跳跃表的末尾。
ConcurrentSkipListMap源码分析
ConcurrentSkipListMap类图
数据节点
static final class Node<K,V> {
final K key;
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;
}
...
}
层级节点
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;
}
...
}
查找节点
我们来看下查找9的流程:
head(4)->4(4)->4(3)->4(2)->7(2)->7(1)->9
ConcurrentSkipListMap.findPredecessor()
找到与key距离最近的一个点,如果没找到返回头结点
// 找到与key距离最近的一个点,如果没找到返回头结点
private Node<K,V> findPredecessor(Object key, Comparator<? super K> cmp) {
if (key == null)
throw new NullPointerException(); // don't postpone errors
for (;;) {
// 从头结点开始,q就是要找的节点,r是q同层级的右节点,d是q的下一个层级的节点
for (Index<K,V> q = head, r = q.right, d;;) {
if (r != null) {
// n是r的数据节点
Node<K,V> n = r.node;
K k = n.key;
if (n.value == null) {
if (!q.unlink(r))
break; // restart
r = q.right; // reread r
continue;
}
// 比较key的大小,key>k则q指q的右节点,继续下一次循环
if (cpr(cmp, key, k) > 0) {
q = r;
r = r.right;
continue;
}
}
// 表示已经到最底层了
if ((d = q.down) == null)
return q.node;
// 如果r==null || key<=k,则q指向q下一层级的节点
q = d;
r = d.right;
}
}
}
ConcurrentSkipListMap.get()
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;
}
插入结点
明白了查找的原理后,插入、删除就容易理解了。为了保存跳表的有序性,所以分三步:查找合适位置——进行插入/删除——更新跳表指针,维护层级性。
ConcurrentSkipListMap.doPut()
private V doPut(K key, V value, boolean onlyIfAbsent) {
Node<K,V> z; // added node
if (key == null)
throw new NullPointerException();
Comparator<? super K> cmp = comparator;
outer: for (;;) {
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;
if ((v = n.value) == null) { // n is deleted
n.helpDelete(b, f);
break;
}
if (b.value == null || v == n) // b is deleted
break;
if ((c = cpr(cmp, key, n.key)) > 0) {
b = n;
n = f;
continue;
}
if (c == 0) {
if (onlyIfAbsent || n.casValue(v, value)) {
@SuppressWarnings("unchecked") V vv = (V)v;
return vv;
}
break; // restart if lost race to replace value
}
// else c < 0; fall through
}
z = new Node<K,V>(key, value, n);
if (!b.casNext(n, z))
break; // restart if lost race to append to b
break outer;
}
}
删除结点