原理 - “二分查找”的链表
二分查找专指对排序后的数组,通过每次查找中间元素,从而支持以log(n)
的时间复杂度的快速查找。但是数组的插入操作是O(n)
的时间复杂度,面对复杂的需求,需要有“二分查找”的链表来同时支持快速的插入、删除。
此时可以有两个选择:
- 平衡二叉树(n叉树)
- 跳表 SkipList
今天我们的主角是跳表(SkipList),树(Tree)就不赘述了。通过下图,我们可以看到跳表的结构:
层数由高到低
Head nodes Index nodes
+-+ right +-+ +-+
|2|---------------->|D|--------------------->|I|->null
+-+ +-+ +-+
| down | |
v v v
+-+ +-+ +-+ +-+ +-+ +-+
|1|----------->|C|->|D|------>|F|----------->|I|------>|K|->null
+-+ +-+ +-+ +-+ +-+ +-+
v | | | | |
Nodes next v v v v v
+-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+
|0|->|A|->|B|->|C|->|D|->|E|->|F|->|G|->|H|->|I|->|J|->|K|->null
+-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+
如果我们要查找G
(查找路径用两条杠标出),需要从最顶层的头节点到D
,然后向下,发现 D < G < I
,然后再向下,向右到了F
,发现 F < G < I
,然后继续向下(下一层的F
)向右,最终找到了G
:
Head nodes Index nodes
+-+ right +-+ +-+
|2|================>|D|--------------------->|I|->null
+-+ +-+ +-+
| down ║ |
v v v
+-+ +-+ +-+ +-+ +-+ +-+
|1|----------->|C|->|D|======>|F|----------->|I|------>|K|->null
+-+ +-+ +-+ +-+ +-+ +-+
v | | ║ | |
Nodes next v v v v v
+-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+
| |->|A|->|B|->|C|->|D|->|E|->|F|=>|G|->|H|->|I|->|J|->|K|->null
+-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+
跳表通过随机的索引来实现近似log(n)
的查找,通过最底层的链表实现O(1)
的插入、删除,这就是跳表的核心。
Java实现 - ConcurrentSkipList
由Java通常的命名规范我们知道,ConcurrentSkipList是并发跳表,并且其实现是通过CAS实现的无锁容器,所以代码上会复杂一些,接下来我们图说跳表。
数据增删
通过几张图,我们就能知道ConcurrentSkipList的数据增删是如何工作的。
增加一个n
非常简单直接:
+------+ +------+
... | b |-------------------->| f | ...
+------+ +------+
|----------------------------|
| v
+------+ +------+ +------+
... | b | | n |----->| f | ...
+------+ +------+ +------+
+------+ +------+ +------+
... | b |------>| n |----->| f | ...
+------+ +------+ +------+
删除一个n
有点复杂:
+------+ +------+ +------+
... | b |------>| n |----->| f | ...
+------+ +------+ +----+
+------+ +------+ +------+
... | b |------>| null |------>| f | ...
+------+ +------+ +------+
+------+ +------+ +------+ +------+
... | b |------>| null |----->|marker|------>| f | ...
+------+ +------+ +------+ +------+
+------+ +------+
... | b |----------------------------------->| f | ...
+------+ +------+
总共三步:
- 设置value为null
- 增加一个marker节点,表示这个节点应该被删除
- 更新前序节点的next指针
其他线程在查找、遍历等过程中,都会并发的协助完成删除的后面两步(work-steal),所以并不一定是当前线程完成的后两步。
可是删除为什么要这么复杂呢?
为什么删除需要如此麻烦的处理?
让我们看个例子就明白了。
假设我们原来的链表长这样:
+------+ +------+ +------+
... | 1 |------>| 3 |----->| 5 | ...
+------+ +------+ +------+
如果我们在删除3时,简单直接的话,就是这样直接使用CAS更新1.next
:
|---------------------------|
| v
+------+ +------+ +------+
... | 1 | | 3 |----->| 5 | ...
+------+ +------+ +------+
但是如果与此同时,我们插入了4
(通过CAS更新3.next
):
|---------------------------|
| v
+------+ +------+ +------+
... | 1 | | 3 | | 5 | ...
+------+ +------+ +------+
| ^
| +---|--+
|-------->| 4 |
+------+
我们会发现,新插入的元素4
丢失了,即使我们通过CAS保证了1.next
和3.next
的正确性,但是我们还是丢失了元素,说明这样删除(或者插入)的无锁算法是错误的。
那么要如何解决呢?
- 可能我们下意识的想要增加一个
3.pre
来感知节点已经被删除了,但是这样就变成双向链表了,维护并发更新变得更加复杂; - 其实
3
的前序就是1.next
,所以,如果能同时比较1.next
和3.next
是否发生了变化,再set就可以了。但是目前计算机并不支持这种双重比较,所以也不行;
那么直接修改next指针为什么会出问题呢?问题的本质在于,插入和删除虽然针对的是同一个节点(都是针对3
),但是却不是同一个字段(1.next
和3.next
),所以CAS失效了(因为CAS只能针对一个值)。那么,如果,插入和删除,都通过3.next
来做并发控制,不是就可以了吗?这就是这个算法的核心。
原论文(A Pragmatic Implementation of Non-Blocking Linked Lists)通过先标记被删除节点的next
指针,再修改前序节点的next
:
+------+ +------+ +------+
... | 1 |------>| 3|X|----->| 5 | ...
+------+ +------+ +------+
此时如果4
要插入,就会发现3.next
是更新过的了(标记删除是对这个指针进行修改),CAS失败,不能插入,需要重新查找插入位置(1.next
):
+------+ +------+ +------+
... | 1 |------>| 3|X|----->| 5 | ...
+------+ +------+ +------+
^
CAS(3.next) +---|--+
-------->| 4 |
+------+
Java的实现中,考虑到效率问题,没有使用AtomicMarkableReference来支持node.next
的做标记,而是通过追加一个marker
节点来实现。
索引增删
索引新增发生在新数据插入后,逻辑就是先构建垂直的链表(固定的down
),再把这个链表,插入二维的跳表中。 例如当我们已经通过索引,找到了插入点,并且将D
插入到了底层的数据链表中。现在要插入F节点的索引了。我们随机确定L=2
,于是构建了如下索引节点(注意,索引节点指向的是同一个数据对象,并非多个D
):
+-+
|D| // 最高层:2
+-+
|
v
+-+
|D|
+-+
|
v
+-+
|D| // 最底层:0
+-+
其实此时最底层已经在链表中了(因为是先插入数据,再构建索引):
Head nodes Index nodes
+-+ right +-+
|2|----------------------------------------->|I|->null
+-+ +-+ +-+ +-+
| down |D|-->|D|--- |
v +-+ +-+ | v
+-+ +-+ | +-+ +-+ +-+
|1|----------->|C|---|------->|F|----------->|I|------>|K|->null
+-+ +-+ | +-+ +-+ +-+
v | | | | |
Nodes next v v v v v
+-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+
| |->|A|->|B|->|C|->|D|->|E|->|F|->|G|->|H|->|I|->|J|->|K|->null
+-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+
将最顶层的D
链接进跳表中之后:
Head nodes Index nodes
+-+ right +-+ +-+
|2|---------------->|D|--------------------->|I|->null
+-+ +-+ +-+ +-+
| down |-->|D| |
v +-+ v
+-+ +-+ | +-+ +-+ +-+
|1|----------->|C|->------|- >|F|----------->|I|------>|K|->null
+-+ +-+ ____| +-+ +-+ +-+
v | | | | |
Nodes next v v v v v
+-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+
| |->|A|->|B|->|C|->|D|->|E|->|F|->|G|->|H|->|I|->|J|->|K|->null
+-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+
下一层链接进入之后:
Head nodes Index nodes
+-+ right +-+ +-+
|2|---------------->|D|--------------------->|I|->null
+-+ +-+ +-+
| down | |
v v v
+-+ +-+ +-+ +-+ +-+ +-+
|1|----------->|C|->|D|------>|F|----------->|I|------>|K|->null
+-+ +-+ +-+ +-+ +-+ +-+
v | | | | |
Nodes next v v v v v
+-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+
| |->|A|->|B|->|C|->|D|->|E|->|F|->|G|->|H|->|I|->|J|->|K|->null
+-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+
索引删除前提是数据删除,例如我们删除了D
的话,因为索引都是引用的同一个数据节点,都会变为空:
Head nodes Index nodes
+-+ right +-+ +-+
|2|---------------->| |--------------------->|I|->null
+-+ +-+ +-+
| down | |
v v v
+-+ +-+ +-+ +-+ +-+ +-+
|1|----------->|C|->| |------>|F|----------->|I|------>|K|->null
+-+ +-+ +-+ +-+ +-+ +-+
v | | | | |
Nodes next v v v v v
+-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+
| |->|A|->|B|->|C|->| |->|E|->|F|->|G|->|H|->|I|->|J|->|K|->null
+-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+
然后后续的查找过程中,遇到空节点都会直接从链表中移除:
Head nodes Index nodes
+-+ right +-+
|2|----------------------------------------->|I|->null
+-+ +-+ +-+
| down | |------| |
v +-+ v v
+-+ +-+ +-+ +-+ +-+ +-+
|1|----------->|C|->| |------>|F|----------->|I|------>|K|->null
+-+ +-+ +-+ +-+ +-+ +-+
v | | | | |
Nodes next v v v v v
+-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+
| |->|A|->|B|->|C|->| |->|E|->|F|->|G|->|H|->|I|->|J|->|K|->null
+-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+
```
Head nodes Index nodes
+-+ right +-+
|2|----------------------------------------->|I|->null
+-+ +-+ +-+ +-+ +-+
| down | |------->| | ->| | |
v +-+ +-+ +-+ v
+-+ +-+ +-+ +-+ +-+
|1|----------->|C|----------->|F|----------->|I|------>|K|->null
+-+ +-+ +-+ +-+ +-+
v | | | |
Nodes next v v v v
+-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+
| |->|A|->|B|->|C|------>|E|->|F|->|G|->|H|->|I|->|J|->|K|->null
+-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+
```
有同学可能有疑问,为什么删除索引没有像删除数据一样,采用marker的方式,而是直接删除了,这样不会导致问题吗? 答案是会!但是不会影响正确性,我们继续看例子。假如我们在删除D
的同时,插入了E
的索引,最顶层(第二层)已经正确的插入了,但是插入第1层索引时,遇到了并发问题:
Head nodes Index nodes
+-+ right +-+ +-+
|2|--------------------->|E|---------------->|I|->null
+-+ +-+ down +-+ +-+
| down | |--------| | |
| +-+ |----|----|----| |
v | v v v v
+-+ +-+ +-+ +-+ +-+ +-+ +-+
|1|----------->|C| | |->|E|->|F|----------->|I|------>|K|->null
+-+ +-+ +-+ +-+ +-+ +-+ +-+
v | | | | | |
Nodes next v v v v v v
+-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+
| |->|A|->|B|->|C|->| |->|E|->|F|->|G|->|H|->|I|->|J|->|K|->null
+-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+
如同我们之前在描述删除数据时举的例子,第一层索引中的E
被链接到了已经被删除的节点(原来的D
),我们把删除先进行完,方便查看新的结构:
Head nodes Index nodes
+-+ right +-+ +-+
|2|--------------------->|E|---------------->|I|->null
+-+ +-+ +-+
| down v |
| +-+ |
v |E|---v v
+-+ +-+ +-+ +-+ +-+ +-+
|1|----------->|C|--------|-->|F|----------->|I|------>|K|->null
+-+ +-+ | +-+ +-+ +-+
v | | | | |
Nodes next v v v v v
+-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+
| |->|A|->|B|->|C|------>|E|->|F|->|G|->|H|->|I|->|J|->|K|->null
+-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+
我们可以发现本来应该在第一层索引的E
的节点,并不在完全在第一层(从第一层开始向右遍历,并不会遇到E
;由上层的E
可以正常通过E
查找到F
.)。但是我们注意到,跳表本身的性质(即每层都是排序的,纵向元素一致,纵向向右可以找到更大的元素)并没有被破坏,只是可能查找性能会略有影响。这便是JavaConcurrentSkipList
的在插入性能和查询性能之间的折中。
ConcurrentSkipList JDK8 源码详细解析
源码部分比较长,解析将通过代码中的中文注释(英文注释为源码自带)进行,方便大家理解。
我们先把跳表的结构再回顾一下,方便我们理解源码:
层数由高到低
Head nodes Index nodes
+-+ right +-+ +-+
|2|---------------->|D|--------------------->|I|->null
+-+ +-+ +-+
| down | |
v v v
+-+ +-+ +-+ +-+ +-+ +-+
|1|----------->|C|->|D|------>|F|----------->|I|------>|K|->null
+-+ +-+ +-+ +-+ +-+ +-+
v | | | | |
Nodes next v v v v v
+-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+
| |->|A|->|B|->|C|->|D|->|E|->|F|->|G|->|H|->|I|->|J|->|K|->null
+-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+
基础的数据结构:
/*
跳表最高层头节点
*/
private transient volatile HeadIndex<K,V> head;
static final class HeadIndex<K,V> extends Index<K,V> {
final int level; //层高(数据在0层,1-MAX为索引
...
}
static class Index<K,V> {
final Node<K,V> node; //索引中的数据节点,纵向节点指向同一个数据对象,方便一同修改
final Index<K,V> down; //下面的索引节点
volatile Index<K,V> right; //右边的索引节点
...
}
局部变量常见含义:
Node: b, n, f 前序节点,当前节点,后续节点
Index: q, r, d 当前索引节点,右节点,下节点.
t 另一个索引节点
Head: h
Levels: j
Keys: k, key
Values: v, value
Comparisons: c
增删都用到的关键方法:查找前序节点(顺便清理删除节点)
private Node<K,V> findPredecessor(Object key, Comparator<? super K> cmp) {
if (key == null)
throw new NullPointerException(); // don't postpone errors
for (;;) {// 循环直到直接return
for (Index<K,V> q = head, r = q.right, d;;) { //从head开始往右查找
if (r != null) {// 没到某一层的最右端
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;
}
if (cpr(cmp, key, k) > 0) {//key大于右节点,向右移动
q = r;
r = r.right;
continue;
}// 找到了第一个不比key大的节点:q < key <= r
}
if ((d = q.down) == null)//向下查找
return q.node;//到达了最底层
q = d;
r = d.right;
}
}
}
觉得不好理解的话,可以结合查找G
的例子的图再看:
Head nodes Index nodes
+-+ right +-+ +-+
|2|================>|D|--------------------->|I|->null
+-+ +-+ +-+
| down ║ |
v v v
+-+ +-+ +-+ +-+ +-+ +-+
|1|----------->|C|->|D|======>|F|----------->|I|------>|K|->null
+-+ +-+ +-+ +-+ +-+ +-+
v | | ║ | |
Nodes next v v v v v
+-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+
| |->|A|->|B|->|C|->|D|->|E|->|F|=>|G|->|H|->|I|->|J|->|K|->null
+-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+
新增(put(k, v)
)的核心逻辑,包含新增数据和新增索引:
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 (;;) { // 无限循环,直到插入成功,通过 break outer 退出
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); // 协助添加删除marker或者去除节点n
break;
}
if (b.value == null || v == n) // b is deleted
break; // v == n 说明 n.value 是一个节点,是marker
if ((c = cpr(cmp, key, n.key)) > 0) { // 如果 key > n.key, 继续向右
b = n;
n = f;
continue;
}
if (c == 0) {// 如果 key == n.key,找到了相同的key
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
}
// 满足:b.key < key < n.key,找到了插入点
z = new Node<K,V>(key, value, n);
if (!b.casNext(n, z))// cas修改
break; // restart if lost race to append to b
break outer; //结束循环,继续更新索引
}
}
int rnd = ThreadLocalRandom.nextSecondarySeed(); //获取随机数
// 插入索引(1/4概率)
if ((rnd & 0x80000001) == 0) { // test highest and lowest bits
// rnd == 0?
// 最多31层
int level = 1, max;
while (((rnd >>>= 1) & 1) != 0) // 第level位bit是1的话,继续循环;0的话,结束
++level;
Index<K,V> idx = null;
HeadIndex<K,V> h = head;//最上层的头节点
if (level <= (max = h.level)) {// 插入层级小于现有最高层级
for (int i = 1; i <= level; ++i)
idx = new Index<K,V>(z, idx, null);//构建从leve->1向下的index链表
}
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];
for (int i = 1; i <= level; ++i)
idxs[i] = idx = new Index<K,V>(z, idx, null);//构建index节点向下的链表
for (;;) {
h = head;
int oldLevel = h.level;
if (level <= oldLevel) // lost race to add level
break;
HeadIndex<K,V> newh = h;
Node<K,V> oldbase = h.node;
for (int j = oldLevel+1; j <= level; ++j)
//以新加元素作为右节点,原头节点做down节点,构建二维跳表索引
newh = new HeadIndex<K,V>(oldbase, newh, idxs[j], j);
if (casHead(h, newh)) {//设置新头节点
h = newh;
idx = idxs[level = oldLevel];
break;
}
}
}
// find insertion points and splice in
splice: for (int insertionLevel = level;;) {
int j = h.level;
for (Index<K,V> q = h, r = q.right, t = idx;;) {
if (q == null || t == null)//找到了当前level的最右侧
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);
if (n.value == null) {//index-r被标记删除
if (!q.unlink(r))
break;
r = q.right;
continue;
}
if (c > 0) {//插入的key在比r大,继续往右
q = r;
r = r.right;
continue;
}
}
// 达成:q < key < r
if (j == insertionLevel) {// 当前是插入层
if (!q.link(r, t))
break; // restart
if (t.node.value == null) {
findNode(key);
break splice;
}
if (--insertionLevel == 0)//插入层减一;到达最底层,退出
break splice;
}
// 在新增索引的纵向范围内,t更新为下一层,以便插入下一层的链表中
if (--j >= insertionLevel && j < level)
t = t.down;
q = q.down; //向下搜索
r = q.right;
}
}
}// else : rnd中0、31位是1,不插入索引(3/4概率)
return null;
}
总结一下,索引新增逻辑:
- 新增
z
节点 - 通过随机,确定其插入层级
L
- 构建该节点,从0层到
L
的索引链表(垂直方向) - 如果
L > head.level
(最高层)- 每层构建
head -> z
索引
- 每层构建
- 将
1 -> min(L, head.level)
索引插入每一层- 找到第一个比
z
大元素,链接进去
- 找到第一个比
删除KV的核心逻辑
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) // key对应的节点已经被删除
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; // v == n: 你是deletion marker
if ((c = cpr(cmp, key, n.key)) < 0) // key已经被删除,结束
break outer;
if (c > 0) {// 有并发插入发生,继续向右
b = n;
n = f;
continue;
}
if (value != null && !value.equals(v))//value不符合
break outer;
if (!n.casValue(v, null))// 标记value为null
break;//并发标记失败
// 标记null成功
if (!n.appendMarker(f) || !b.casNext(n, f))
// 既没有增加marker成功,也没有去除n成功,重试
findNode(key); // retry via findNode
else {
findPredecessor(key, cmp); // clean index //遇到空节点顺便从跳表中移除
if (head.right == null)//被删除的节点是该层唯一节点,tryReduceLevel
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;
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 // 如果并发使得本层不为空了,恢复head
}
参考: