yield-bytes

沉淀、分享与无限进步

Java高级主题:深度讨论高并发跳表数据结构ConcurrentSkipListMap的源代码实现(上)

在单线程场景下,HashMap适用于key为无序的键值对存放场景,而TreeMap适用于key为有序的键值对存放场景。

在高并发场景下,ConcurrentHashMap适用于key为无序的键值对存场景,但对于高并发且要求key有序的场景下,TreeMap非线程安全显然无法满足此场景, 在Concurrent包里面只有跳表:ConcurrentSkipListMap可以满足”基于乐观锁高性能的并发读写、key有序”的需求,而且其设计不会像ConcurrentHashMap这么复杂,但确有着恰当的应用场景,例如对于时序流式数据的存放(最近比较热门的物联网大数据引擎TDengine),可以将乱序的记录以时间戳作为key插入到跳表中,跳表内部处理插入时会比较key的hash值大小以找到节点合适的插入位置,那么在读取时跳表返回的记录就是有序了。

jdk1.8的ConcurrentSkipListMap在本文简写为CSM,Dung Lea在源代码开头的注释详细介绍了CSM总体设计思路并给出字符型展示的CSM结构图,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
* Head nodes          Index nodes
* +-+ right +-+ +-+
* |2|---------------->| |--------------------->| |->null
* +-+ +-+ +-+
* | down | |
* v v v
* +-+ +-+ +-+ +-+ +-+ +-+
* |1|----------->| |->| |------>| |----------->| |------>| |->null
* +-+ +-+ +-+ +-+ +-+ +-+
* v | | | | |
* Nodes next v v v v v
* +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+
* | |->|A|->|B|->|C|->|D|->|E|->|F|->|G|->|H|->|I|->|J|->|K|->null
* +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+

CSM源代码解析文章说明:由于CSM解析内容较多,因此全文分为“深度讨论高并发跳表数据结构ConcurrentSkipListMap的源代码实现(上)”和“深度讨论高并发跳表数据结构ConcurrentSkipListMap的源代码实现(下)”两篇文章
上篇关注的重点:CSM数据结构设计原理、doGet、doPut核心方法解析
下篇关注的中断:doRemove核心方法解析、总结

在这里插入图片描述

《gitee 博客文章封面》

CSM的基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ConcurrentSkipListMap;

public class SkipListDemo {
public static void main(String[] args) throws InterruptedException {
// ConcurrentHashMap<String,Integer> sm= new ConcurrentHashMap<>();
ConcurrentSkipListMap<String,Integer> sm=new ConcurrentSkipListMap<>();
List<Thread> list=new ArrayList<>();
for (int i = 0; i <10 ; i++) {
Thread t=new Thread(new Runnable() {
@Override
public void run() {
sm.put(Thread.currentThread().getName(),(int)Thread.currentThread().getId());
}
});
list.add(t);
}

for (Thread thread : list) {
thread.start();
}
for (Thread thread : list) {
thread.join();
}
System.out.println(sm);
}
}

使用CHM输出的结果为无序结果:

1
{Thread-3=12, Thread-4=13, Thread-5=14, Thread-6=15, Thread-7=16, Thread-8=17, Thread-9=18, Thread-0=9, Thread-1=10, Thread-2=11}

使用CSM输出的结果为有序结果:

1
{Thread-0=9, Thread-1=10, Thread-2=11, Thread-3=12, Thread-4=13, Thread-5=14, Thread-6=15, Thread-7=16, Thread-8=17, Thread-9=18}

CSM 数据结构图

在这里插入图片描述

这个跳表结构有四层,其中base-level层是完整的数据节点链表,在base-level层上面的三层都是索引节点构成的索引链,从图中可以直观看到:上层的索引链表是下层索引链表的“快车道”

从这种图也可以总结出跳表(这里是泛指跳表结构不是指Java的CSM)的基本特征:

  • 由最底层的完整数据节点链表层+上面的多层索引层组成。
  • 每一层都是一个有序的链表
  • 如果一个key出现在第n层中,则它在最底层以及1到第n-1层也都会出现

而对于jdk1.8实现的功能完整的ConcurrentSikpListMap,可按上面的结构图进行简单说明各个元素的构成(最底层到最顶层):

  • Node节点:存放数据的节点,只在最底层的数据层链表出现,在插入操作时,可能会被随机算法选中上升为“index索引节点”

  • base-level:指代数据层链表的位置,此层存放的是完整数据节点链表,该层的所有节点都是Node类型,不含有Index类型节点!

  • BASE_HEADER:这个节点不是数据层链表的第一个数据节点,它是辅助节点,可以看做是数据层链表的“索引节点”,但它是Node类型:new Node<K,V>(null, BASE_HEADER, null)

  • Index节点:位于索引层的节点,采用随机算法把它插入在数据层上方的索引层链表上。

  • HeadIndex节点:作为辅助节点,非数据节点,是当前索引层链表的头节点,它也会指向下一层index节点索引链表的头节点。Dung Lea称它是dummy node
  • level:索引节点所在层序号,从level=1开始计数,最高不超过31(含31层),文章后面给出了计算解释。
  • right指针:HeadIndex、Index节点专有字段,可以使得遍历链表的方向向右移动
  • down指针:HeadIndex、Index节点专有字段,可以使得遍历链表的方向向下移动
  • head节点:所有线程的读写操作都是这个头节点开始作为遍历入口,就像“迷宫的入口点,然后根据向右边还是向下移动,直到走到适合索引节点位置”,它指向最顶层索引层的HeadIndex节点。
  • node指针:此指针最特殊!! 因为每个数据节点,它垂直上方的所有索引节点的node指针都指向数据节点本身,new Index<K,V>(node=新插入的数据节点,down=下一层索引节点, right=当前索引节点的后继索引节点),注意到上图结构图中,level=1层的索引节点与base-level数据层的数据节点之间不是通过down指针连接的,而是通过node指针连接的,这点务必在后面源码分析反复想起,否则理解错了CSM结构图,那么就无法正确解析源代码设计。

还有marker标记型节点,它在删除节点的时机会被使用。

相关节点的定义:

数据节点:

Node类除了构造器,还定义了一些基本读写方法,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
/** node是数据节点,内部存放了key和value,node节点能构成有序链表
* Nodes hold keys and values, and are singly linked in sorted
* order, possibly with some intervening marker nodes. The list is
* headed by a dummy node accessible as head.node. The value field
* is declared only as Object because it takes special non-V
* values for marker and header nodes.
*/
static final class Node<K,V> {
final K key; // 注意final修饰
volatile Object value; // 注意volatile修饰,因为cas会操作它
volatile Node<K,V> next;// 注意volatile修饰,因为cas会操作它

/** 用于创建正常的数据节点,可以看到它没有right字段和down字段,因为它就是位于底层的数据链表中,是最熟悉链表普通节点
* Creates a new regular node.
*/
Node(K key, Object value, Node<K,V> next) {
this.key = key;
this.value = value;
this.next = next;
}

/**
* Creates a new marker node. A marker is distinguished by
* having its value field point to itself. Marker nodes also
* have null keys, a fact that is exploited in a few places,
* but this doesn't distinguish markers from the base-level
* header node (head.node), which also has a null key.
*/
// 使用Node类型创建marker节点(在删除操作会被使用),和数据节点的区别:无key,且value指向自己。
Node(Node<K,V> next) {
this.key = null;
this.value = this;
this.next = next;
}

/** 使用cas更改当前节点value的值
* compareAndSet value field
*/
boolean casValue(Object cmp, Object val) {
return UNSAFE.compareAndSwapObject(this, valueOffset, cmp, val);
}

/** 使用cas更改当前节点的next指向
* compareAndSet next field
*/
boolean casNext(Node<K,V> cmp, Node<K,V> val) {
return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}

/**
* Returns true if this node is a marker. This method isn't
* actually called in any current code checking for markers
* because callers will have already read value field and need
* to use that read (not another done here) and so directly
* test if value points to node.
*
* @return true if this node is a marker node
*/
//因为HeadIndex的key也null,因此使用value==this才能唯一区别出这是marker节点
boolean isMarker() {
return value == this;
}

/** 用于判断node节点是否就是base-level的HeadIndex节点
* Returns true if this node is the header of base-level list.
* @return true if this node is header node
*/
boolean isBaseHeader() {
return value == BASE_HEADER;
}

/** 使用cas将一个marker节点放在当前节点的后面,其中f节点是当前节点的后继节点
将:
node->f->...
变成:
node->marker->f->...
* Tries to append a deletion marker to this node.
* @param f the assumed current successor of this node
* @return true if successful
*/

boolean appendMarker(Node<K,V> f) {
return casNext(f, new Node<K,V>(f));
}

/** 当前节点通过添加marker节点来帮助删除操作或者用于将当前节点脱离前驱节点。
* Helps out a deletion by appending marker or unlinking from
* predecessor. This is called during traversals when value
* field seen to be null.
* @param b predecessor
* @param f successor
*/
// 如果在这里不能理解其含义,可在后面的删除操作中关于(b,n,f)三个节点调整解析中得到回答。
void helpDelete(Node<K,V> b, Node<K,V> f) {
/*
* Rechecking links and then doing only one of the
* help-out stages per call tends to minimize CAS
* interference among helping threads.
*/
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);
}
}

/** 返回数据节点key对应的value,如果当前节点value字段指向自己说明是一个mark节点,则返回null,如果当前节点value字段BASE_HEADER,说明是底层的HeadIndex节点也是返回null
* Returns value if this node contains a valid key-value pair,
* else null.
* @return this node's value if it isn't a marker or header or
* is deleted, else null
*/
V getValidValue() {
Object v = value;
if (v == this || v == BASE_HEADER)
return null;
@SuppressWarnings("unchecked") V vv = (V)v;
return vv;
}
索引节点Index

Index节点位于“快车道——索引层链表”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91

/* ---------------- Indexing -------------- */

/**
* Index nodes represent the levels of the skip list. Note that
* even though both Nodes and Indexes have forward-pointing
* fields, they have different types and are handled in different
* ways, that can't nicely be captured by placing field in a
* shared abstract class.
*/
// 可以看到Index索引节点的下一个节点指针不是next字段而是right字段,但他们作用都是指向后继节点,取用不同的名称是为了区别Node的next字段、方便理解和阅读。
static class Index<K,V> {
/* 相关指针的局部指向示意图:
this → right → indexNode → indexNode → null

down (Index<K,V>类型)

node (最底层的数据节点)
*/
final Node<K,V> node; //索引节点在垂直方向上指向的最底层的对应数据节点,Node类型
final Index<K,V> down; //注意 down指针是Index索引节点类型,不是数据节点Node类型
volatile Index<K,V> right;//注意 right指针是Index索引节点类型,不是数据节点类型

/**
* Creates index node with given values.
*/
// Index位于索引层的链表,那么它的down字段指向下层的Index节点(或者最底层的node数据节点),right字段指向后继索引节点
Index(Node<K,V> node, Index<K,V> down, Index<K,V> right) {
this.node = node;
this.down = down;
this.right = right;
}

/** 使用cas设置Index节点的right节点:在插入操作和删除操作使用
* compareAndSet right field
*/
final boolean casRight(Index<K,V> cmp, Index<K,V> val) {
return UNSAFE.compareAndSwapObject(this, rightOffset, cmp, val);
}

/**
* Returns true if the node this indexes has been deleted.
* @return true if indexed node is known to be deleted
*/
final boolean indexesDeletedNode() {
return node.value == null;
}

/**
参考后文
* Tries to CAS newSucc as successor. To minimize races with
* unlink that may lose this index node, if the node being
* indexed is known to be deleted, it doesn't try to link in.
* @param succ the expected current successor
* @param newSucc the new successor
* @return true if successful
*/

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);
}

/**
参考后文
* Tries to CAS right field to skip over apparent successor
* succ. Fails (forcing a retraversal by caller) if this node
* is known to be deleted.
* @param succ the expected current successor
* @return true if successful
*/
final boolean unlink(Index<K,V> succ) {
return node.value != null && casRight(succ, succ.right);
}

// Unsafe mechanics,Index类内部的Unsafe实例创建
private static final sun.misc.Unsafe UNSAFE;
private static final long rightOffset;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> k = Index.class;
rightOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("right"));
} catch (Exception e) {
throw new Error(e);
}
}
}

HeadIndex节点
1
2
3
4
5
6
7
8
9
10
11
12
/* ---------------- Head nodes -------------- */

/** 从结构图也可以看出:该层HeadIndex的right指针指向的就是该层链表的头节点,down指针指向下一层的HeadIndex节点,关键的level属性:表示该层的序号,最小值是1,也即第一层
* Nodes heading each level keep track of their 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;
}
}

get方法解析

有了以上基本数据结构说明后,理解查找节点的过程来进一步解析CSM核心设计。

1
2
3
public V get(Object key) { 
return doGet(key); // get方法内部封装的私有的doGet方法
}

doGet总体思路:使用comparator比较器,先找到key对于的前驱节点b,此刻起,有(b,n,f)三个节点关系(b:当前节点n的前驱节点,f:当前节点n的后继节点),这三个指针非常重要,要想读到准确n节点的value,显然在第一次读到(b,n,f)时,还需要再次检查b节点、n节点有无被其他线程修改或者删除,确保前后两次读一致,才可以正常地把key和n节点进行比较,若key==n.key,则可查询到key对应的值。

b节点:给定key的前驱节点,一般有b.key<key;n节点:要与key作为比较,f节点:n节点后驱节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
private V doGet(Object key) {
if (key == null)
throw new NullPointerException();
Comparator<? super K> cmp = comparator;
/* 外层循环的outer非常有用:若当前读线程在内部循环找到(b,n,f)且准备读取数据节点n,有其他写线程(如删除操作)抢先一步将正常的数据n节点标记为marker或者更改了n节点,那么就需要要求当前读线程重新跳回外层循环再次定位(b,n,f)这个三个特殊节点,或者说:如果在内层循环,当前线程前后读取的(b,n,f)三个节点中的b节点、n节点有出现不一致读,则需要再次遍历,直到前后读取的(b,n,f)中的b、n都未改动时,返回的value才是key对应的value。
*/
outer: for (;;) {
// ① 在内循环开始时,找到给定key的前驱节点b,以及b节点的next节点:n节点,b节点key一定是小于给定的key,否则b节点就不是给定key的前驱节点
for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
Object v; int c; // c是comparator的比较结果,0,小于0,大于0
//② 来到数据层链表的尾部位置如: b → null,因此n指向null,说明key未能在数据层链表找到,可直接返回。
// 这里也对应下面③b=n;n=f后移操作,也是作为外层循环首要考虑的程序出口条件。
if (n == null)
break outer; // 注意:break outer是指结束结束外层循环来到return null语句
// n节点的后继节点为f节点
Node<K,V> f = n.next;
// 当前时刻n节点已经不是b节点的后继节点,说明n节点被其他线程改过(前、后不一致性读),那么当前线程回到内层for循环① 位置继续下一轮重试。
if (n != b.next) // inconsistent read
break;
// 若线程执行到这里发现n节点value变为null,说明n节点被标记为“删除”,当前线程进入helpDelete帮助删除n节点,然后回到内层for循环①位置继续下一轮重试。
if ((v = n.value) == null) { // n is deleted
n.helpDelete(b, f);
break;
}
// 若线程执行到这里发现b节点value变为null,此时说明前驱节点b被其他线程删除,或者n节点被其他线程标记为marker节点,当前线程只能回到内层for循环①位置继续下一轮重试。
if (b.value == null || v == n) // b is deleted
break;
// 代码执行到这里说明b节点和n节点前后两次读取一致,那么可以开始比较key和n.key,若相等,n节点就是要找的key对应的节点,返回其value即可
if ((c = cpr(cmp, key, n.key)) == 0) {
@SuppressWarnings("unchecked") V vv = (V)v;
return vv;
}
// b节点已经是给定key的前驱节点,有:b.key<key,若c<0,说明key<n.key,因为b节点和n节点之间已经没有节点,既然key对应的不是b节点也不是n节点,说明key节点不在跳表里面,那么当前读线程只能跳出外层循环并结束读取操作,返回null。
if (c < 0)
break outer;
//③ 代码执行到这里,说明要查找的key大于n节点的key,所以要继续向当前链表右边方向遍历,b和n指针同时向右边移动一个节点位置的指向,可以想象:当b来到链表尾部最后一个节点时,此时n=b.next=null 回到for循环就会进入②位置的分支:满足if (n == null),然后break outer退出外层循环后return null
b = n;
n = f;
}
}
return null;
}

doGet方法的设计整体相对清晰,最关键的一个点是如何找到key的前驱节点b?通过findPredecessor实现。

findPredecessor方法实现:

此方法有两个功能:

功能 1:找到给定key对应的前驱节点node

功能2:also unlinks indexes to deleted nodes found along the way,此功能具体分析放在后文的doRemove方法,在本小节中只需关注功能1即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
    /* ---------------- Traversal -------------- */

/**
* Returns a base-level node with key strictly less than given key,
* or the base-level header if there is no such node. Also
* unlinks indexes to deleted nodes found along the way. Callers
* rely on this side-effect of clearing indices to deleted nodes.
* @param key the key
* @return a predecessor of 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、d的指向方向,如下所示
q → r → indexNode → indexNode → null

d
先将q指向head节点,那么r和d自然就出来了,如下所示
q(head) → r (q.right) → indexNode → indexNode → null

d(q.down)
*/
// 这里很关键:q、r、d三个指针都是Index索引节点类型,说明findPredecessor方法只会在索引层去找key的前驱节点,而不会去到最底层的数据节点链表去找。
for (Index<K,V> q = head, r = q.right, d;;) {
//①分支:从上面的q、r位置关系可知,q、r都在同一层索引层移动,如果r节点不为空,说明可以继续向右遍历索引链表,若r来到索引链表尾部null位置,那么执行流跳过①分支来到④分支
if (r != null) {
//取出索引节点指向的数据节点node,此节点的key将和给定key比较
Node<K,V> n = r.node;
K k = n.key;
/*② 此分支建议深度理解了doRemove方法后,再回来理解它则简单很多。
在remove方法中有其他写线程使用n.casValue(v, null)将目标删除节点n的value字段设为null,因此只要读线程进入此分支,说明r索引节点指向的数据节点node已经被其他写线程标记为删除状态,此时要将q节点和r节点断开链接,也即将q → r → r.right 变成q → r.right
*/
if (n.value == null) {
if (!q.unlink(r))
break; // 若unlink内部的cas失败,则继续重试
// 如果上面unlink成功,旧r节点断开q节点,此时重新读取q的right节点,然后继续下一轮循环。
r = q.right; // reread r
continue;
}
//③分支:给定的key比r.key大,说明只好向右继续查找:q、r指针同时右边移动一个节点
if (cpr(cmp, key, k) > 0) {
q = r;
r = r.right;
continue;
}
}
/*④分支:findPredecessor结束点:找到了key的前驱节点。注意这里q已经是第一层的目标索引节点,q.down指向null,q.node指向数据节点node,所以不要误解为q.down指向node,而node不为空,参考后面图文解释!
q (索引层leve=1的索引节点)

d (最底层数据节点),显然此时d是Node节点类型,
*/
if ((d = q.down) == null)
return q.node;
//⑤分支:遍历到当前索引层链表尾部r=null也没找到key的前驱节点,那么只能垂直在向下移动,在下一层索引层的链表头部继续重复以上逻辑。务必记住:此“向下移动一层”的操作只在索引层操作,不会“下移到数据链表层”!
q = d;
r = d.right;
}
}
}

q.unlink(r))的定义:

unlink方法是Index索引节点内部方法,设计目的是:当Index.node指向的数据节点已经被标记删除,那么此Index节点也需要被删除,假设r是当前处理索引节点:

将q → r → r.right 变成q → r.right,对应到unlink的相关变量则为:

将q → succ → succ.right 变成q → succ.right

1
2
3
4
5
6
7
8
9
10
11
12
13

/**
* Tries to CAS right field to skip over apparent successor
若调用此方法时,q恰好被标记为删除状态,那么cas会失败,要求unlink调用者再次循环重试,这就是findPredecessor方法里面的②分支设计为重试的原因。
* succ. Fails (forcing a retraversal by caller) if this node
* is known to be deleted.
* @param succ the expected current successor
* @return true if successful
*/
final boolean unlink(Index<K,V> succ) {
// 这里node.value是指q.node.value,不是r.node.value!要求q指向的数据节点node还没被删除才能进行cas
return node.value != null && casRight(succ, succ.right);
}

findPredecessor方法最关键的一个设计点——返回逻辑:

1
2
3
4
5
6
7
/*④分支:findPredecessor结束点:找到了key的前驱节点
q (索引层leve=1的索引节点)

d (最底层数据节点),显然此时d是Node节点类型,
*/
if ((d = q.down) == null)
return q.node;

以上代码很容易让人产生错误理解(如果提前理解了doPut方法中idx插入索引节点的设计,则能正确理解④分支的设计逻辑),具体说明人下面两张图所示

正确的理解:
在这里插入图片描述

错误的理解:

在这里插入图片描述

虽然在前面CSM的数据结构图中 level=1的索引层节点画的图是直接指向下方数据层节点,但必须要清楚在真正的CSM设计中,是不存在这个down指向关系,你可以理解是第一层索引节点通过q.node指针实现指向最底层的数据节点。

图示doGet方法查找图

分为两步理解:
在这里插入图片描述

doGet方法和findPredecessor的执行流程能够让你理解了CSM,但要想深入掌握它,还得从put方法和remove方法中找到答案

put方法解析

Main insertion method. Adds element if not present, or replaces value if present and onlyIfAbsent is false.

如果key不在csm中可插入key,onlyIfAbsent默认为false,表示如果该key已经在csm中,则替换旧value。

1
2
3
4
5
public V put(K key, V value) {
if (value == null)
throw new NullPointerException();
return doPut(key, value, false);
}

doPut的主要设计思路:

1、找到合适位置插入key节点

  • 1.1 找到key在数据层链表中前驱节点b(这个逻辑在doGet已经解析过),并给出(b,n,f)这三个指针的指向

  • 1.2 将新插入的节点z用cas设置到b的next中,也即将节点z插入到 b → n,也即变成 b → z → n

2、通过随机算法来决定是否要在新插入的节点z上(垂直方向上)构建索引节点(若层数n,就构建n个索引节点),

3、将这些索引节点在各自的索引层链接好左右关系指向。

建议先用单线程的角度去理解doPut主体逻辑,也即假设b节点、n节点不会被其他写线程删除或者更改,这样可以快速理解doPut设计逻辑。

doPut方法第一部分:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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 (;;) {
// 注意:以下内层循环所有逻辑(除了findPredecessor)只发生在最底层的数据链表层中进行遍历
for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
// 1.1 若不是在数据链表尾部,则可以继续遍历,否则也即来到链表尾部就跳到1.2分支
if (n != null) {
Object v; int c;
// f暂存n节点之后子链,用于后续遍历
Node<K,V> f = n.next;
//1.1.1 单线程情况下,n还是b的next节点,若高并发写情况下,n节点可能被其他线程更改,那么当前线程第一次读取的b.next和现在读取的b.next显然不一致,也即“inconsistent read”
if (n != b.next) // inconsistent read
break;
//1.1.2 n的值为null,说明被其他线程标记位删除状态,只能先去helpDelete,然后再回到内层循环重试。
if ((v = n.value) == null) { // n is deleted
n.helpDelete(b, f);
break;
}
//1.1.3 前驱节点b被删除或者n节点value是指向自己(说明n是一个mark节点),前驱节点都被其他线程删除,只能退出本轮回到内层for循环继续重试。
if (b.value == null || v == n) // b is deleted
break;
//1.1.4 给定的key比n节点还大,那么只能继续向右比较
if ((c = cpr(cmp, key, n.key)) > 0) {
// 更新b、n指针向后移动一步
b = n;
n = f;
continue;
}
//1.1.5 如果key==n.key,说明已找到该key。
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
}
/* 1.2
第一种情况:根据1.1 n不为空,那么在 b → n 之间插入 b → z → n
第二种情况:如果1.1 的n节点为空说明来到链表尾部,也即b前驱节点后面就是null,那么b → null 变成b → z → null
*/
z = new Node<K,V>(key, value, n); //创建key这个插入节点,然后有 z → n
// 1.3
if (!b.casNext(n, z)) // cas: 在n节点前、后时刻读一致性时,z节点才能成功CAS插入b前驱节点后面
break; // restart if lost race to append to b若cas失败,只能回到内循环重试
break outer;
}

doput方法第二部分:

在第一部分逻辑插入号z节点后,那么接下里就得考虑z节点垂直方向是否需要创建索引层节点。

如何决定是否需要? 当然是根据随机数算法满足test条件来决定是否需要执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
     /* 这里随机数的生成考虑很细致:ThreadLocalRandom.nextInt也可以实现线程自己本地随机数,而nextSecondarySeed能做到生成和“当前线程业务代码调用的ThreadLocalRandom.nextInt”不冲突的随机数。
* To produce random values without interference across threads,
* we use within-JDK thread local random support (via the
* "secondary seed", to avoid interference with user-level
* ThreadLocalRandom.)
*/
int rnd = ThreadLocalRandom.nextSecondarySeed();

/*
0x80000001是16进制数:1000 0000 0000 0000 0000 0000 0000 0001
考察rnd & 0x80000001 == 0成立时rnd的取值特征:
0nnn nnnn nnnn nnnn nnnn nnnn nnnn nnn0
其中,n表示0或者1,其实就是表达rnd只要是偶数就可使得测试条件通过,所以概率为50%。
*/
if ((rnd & 0x80000001) == 0) { // test highest and lowest bits
int level = 1, max;
/* 1、根据rnd的比特位值为1的特征先计算一个“level值”,
例如0nnn nnnn nnnn nnnn nnnn nnnn nnnn nnn0,30个n全部为1,那么
level=1+30=31,计算出31层,是否就是真的要在跳表上加31层索引节点呢? 显然没那么简单,具体看第2点逻辑
*/

while (((rnd >>>= 1) & 1) != 0)
++level;
// 这一句非常关键:第一层的索引节点Index,它的down指向idx=null,而不是指向其下方的node数据节点。
Index<K,V> idx = null;
HeadIndex<K,V> h = head; // head指向的是最顶层的HeadIndex节点,不是最底层的数据链表头节点
/*
2、如果计算的level未超过现有索引层数,那么好处理:在z节点上方建立level个索引节点
例如,假设当前h.level=3,也即有三层索引层,rnd=2,那么level计算结果为2,因此根据以下逻辑,
在z节点垂直上方创建2个index节点,且每个index的right还是null,换句话说,这些索引节点还未与其所在层的前后索引节点建立指向关系,如下:
index2

index1
|
z
*/
if (level <= (max = h.level)) {
for (int i = 1; i <= level; ++i)
idx = new Index<K,V>(z, idx, null);
}

/*
3、如果level>现有层数,例如level计算值为7,现有层数为3,那么也只能给它加一层,而不是加(7-3)层,
以下max按3作为案例说明
*/
else { // try to grow by one level
// level=3+1=4
level = max + 1; // hold in array and later pick the one to use
// 存放要添加的索引节点数组,level为4,那么idxs容量为5
@SuppressWarnings("unchecked")Index<K,V>[] idxs =
(Index<K,V>[])new Index<?,?>[level+1];
// 注意:这里从idxs的下标1开始放入idx索引节点,idxs[0]不参与实际逻辑。
for (int i = 1; i <= level; ++i)
/* 跟上面2部分类似,但它这里每次创建好的节点放到idxs数组中,方便后面新创建HeadIndex使用:
newh = new HeadIndex<K,V>(oldbase, newh, idxs[j], j);
*/
idxs[i] = idx = new Index<K,V>(z, idx, null);
// 3.1
for (;;) {
h = head;
// 根据3的假设,原来由3层索引层
int oldLevel = h.level;
// 假设当前时刻仅有当前线程添加索引层,那么4<=3,不需要重试。
if (level <= oldLevel) // lost race to add level:有其他插入线程抢先一步升高了索引层,当前线程只能下一轮重试,因此在3.1使用了for循环
break;
// 3.2
HeadIndex<K,V> newh = h;
Node<K,V> oldbase = h.node;
// 创建新添加的索引层的头节点,新增多少层,就添加多少个头节点,
// 由于oldLevel=3,level=4,因此以下计算后只在第4层添加一个HeadIndex,并且该HeadIndex的四个字段都有了指向关系。
for (int j = oldLevel+1; j <= level; ++j)
newh = new HeadIndex<K,V>(oldbase, newh, idxs[j], j); // 这就是为何前面需要创建idxs数组的原因。
// 3.3 将跳表头节点Head指向新添加的HeadIndex
if (casHead(h, newh)) {
h = newh;
// 这里level = oldLevel=3,也即将idx指向第3层新增索引节点,为何不是第4层新增的索引节点呢?因为在3.2中,在第4层新增的HeadIndex节点已经将right指针指向对应新增的索引节点。因此之后要处理的索引节点是从旧level层数开始处理down、right指向关系
idx = idxs[level = oldLevel];
break;
}
}
}

第2部分的逻辑对应下图:

根据key计算的level未超过现有索引层数3时的逻辑图
在这里插入图片描述
第3部分的逻辑对应下图:

根据key计算的level超过现有索引层数3时的逻辑图,不管是计算level=4大于level=3还是其他比3值更大,也只能增加一层索引层:level = max + 1
在这里插入图片描述
在新增加的level=4的索引层,需要添加一个新的HeadIndex节点,根据以下逻辑可知:新增的HeadIndex节点已经将right指针指向对应新增的索引节点,如图位置1所示

1
2
for (int j = oldLevel+1; j <= level; ++j)
newh = new HeadIndex<K,V>(old base, newh, idxs[j], j);
doput方法第三部分:

在doput方法第二部分完成新插入数据节点z对应的多层索引节点创建,但索引节点idx还未与前后已有节点建立指向关系,因此接下里代码逻辑中负责完成此事。

为了能具体化分析以下逻辑,不妨假设插入z节点后,计算出的level=4高于当前跳表oldLevel=3,根据上面3.3的逻辑可知,此时h执行第4层的headIndex,根据idx = idxs[level = oldLevel] ,有idx指针指向第3层的待处理索引节点idx,level=oldLevel=3,根据这些变量代入以下逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
				// find insertion points and splice in.
// splice in 是指拼接的意思,也即将idx加入所在层的链表关系中
//insertionLevel = level =3,
splice: for (int insertionLevel = level;;) {
// 关于insertionLevel的理解,结合上图可知,insertionLevel=3,说明第3层idx加入此层索引层链表中,也称为待插入层。
int j = h.level; // 这个j表征执行流走向第几层(也称为当前处理层),h.level的值为4
// 类似doGet方法,从顶层的左上方head位置开始遍历而且只在索引层去检索,目的是把每个需要insertionLevel层的idx节点t链入q → r之间,变成:q → t → r 。注意:t的方向是从高层idx向低层的idx移动指向,idx:新增加的索引阶段
for (Index<K,V> q = h, r = q.right, t = idx;;) {
//3.1 两种情况可以说明idx链入工作完成:条件1:说明q来到当前索引层的链表末尾位置,q会指向null 2、当t指向null时,垂直方向多个idx节点全部链入对应索引层。
if (q == null || t == null)
break splice;
//3.2 对于q → r → r.right...情况,r不是null时,也即q不是尾节点时
if (r != null) {
// q → r → r.right,也即熟悉的(b,n,f)子链
Node<K,V> n = r.node;
// compare before deletion check avoids needing recheck
int c = cpr(cmp, key, n.key);
// 如果当前索引节点r指向的数据节点n被标记删除,说明索引节点r也需要删除,此时要将q节点和r节点断开链接,也即将q → r → r.right 变成q → r.right。前面findPredecessor方法内部也用到了该unlink方法,此逻辑被称为side-effect!
if (n.value == null) {
if (!q.unlink(r))// q、r断开失败则需要重试
break;
// 取出q新的右节点继续遍历重试
r = q.right;
continue;
}
// r.key<key,只能在同一层继续向右找到key插入的位置。
if (c > 0) {
q = r;
r = r.right;
continue;
}
}
/*3.3 如果当前处理层指针恰好移动到位于需要对新增索引节点建立前后指向关系的待插入层
那么将待处理的索引节点 t=idx 链入q → r 之间,变成:q → t → r
*/
if (j == insertionLevel) {
if (!q.link(r, t))
break; // restart t=idx 链入失败只能回到循环重试。
// 索引节点idx指向的新插入数据节点z已被其他写线程标为删除状态,那么当前线程就不需要给z节点建立垂直方向的索引,相反当前线程利用findNode去帮助把z节点删除,同时findNode里面的findPredecessor方法也会在find过程中沿途清理z节点上方的idxs索引节点,这就是findNode的“side-effect” 思想:去删除数据节点z的同时顺便沿途把与z对应新建的索引节点idxs都删除了
if (t.node.value == null) {
findNode(key);
break splice;
}
// 如果q.link(r, t)成功,且新插入的数据节点z未被删除,那么当前待插入层insertionLevel已完成,如果insertionLevel自减1等于0,说明insertionLevel指向最底层的数据层,显然数据层不需要处理,说明所有idxs都在各自所在的索引层链表里面,可结束。
if (--insertionLevel == 0)
break splice;
} //


// 这里的理解相对有点绕:若当前待插入层已经处理完,则t指向下一个层的待处理索引节点idx。什么场景不需要执行 t = t.down呢?当遍历层指针j在一开始就指向新增的最高层时,t此时是指向(最高层-1)的idx,因此只有当遍历层指针j来到当前待插入层insertionLevel且t已经链入该层的q → r之间,说明当前insertionLevel层已处理完,那么t可以移动下一层的idx
if (--j >= insertionLevel && j < level)
t = t.down;
// 1、当前遍历层指针j指向最高层时 2、当前遍历层指针j指向当前待插入层insertionLevel且insertionLevel层的t节点已经链入该层的q → r之间。这两个条件都会使得q,r下移一层
q = q.down;
r = q.right;
}
}
}
return null;
}

关于q.link(r, t) 的说明

1
2
3
4
5
6
7
8
9
10
11
12
// if (!q.link(r, t))
final boolean link(Index<K,V> succ, Index<K,V> newSucc) {
// 这里的node节点就是索引节点q.node指向的数据节点,succ就是r,newSucc就是t(新增的idx节点)
Node<K,V> n = node;
// t.right=r
newSucc.right = succ;
/* link能成功的两个条件同时成立:
1、要求q指向的node数据节点一定不能处于被标记为"删除状态"
2、q的后继节点r索引节点未被其他写线程标记为"删除状态"
*/
return n.value != null && casRight(succ, newSucc);
}
findNode的作用

此方法在doPut的“第三部分逻辑:新建的索引链表如何插入到该层链表中”起到关键作用,此外它还被doRemove、computeIfPresent、merge、replace等方法在内部调用,因此需要对其设计逻辑进行解析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
  // findNode的源代码注释其实写得很清楚,需要看完一下源码再解析
// 以下逻辑一定要带着这样的前提:新插入的z节点被标记删除后,需要对其上方的新建索引节点都一起删除
private Node<K,V> findNode(Object key) {
if (key == null)
throw new NullPointerException(); // don't postpone errors
Comparator<? super K> cmp = comparator;

outer: for (;;) {
//1、找到key在数据层链表的前驱节点b,从b开始向右遍历。这里最关键:findPredecessor,除了找到z节点的前驱节点b,它的额外收益还把沿途找到的z对应的索引节点都删除了
for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
Object v; int c;
//2、 来到链表末尾则可结束
if (n == null)
break outer;
// 又是经典的(b,n,f)三节点指针
Node<K,V> f = n.next;
//3、 n已不是b的后继节点,重试
if (n != b.next) // inconsistent read
break;
//4、 n被标记为删除状态,则使用helpDelete真正删除n节点,然后重试
if ((v = n.value) == null) { // n is deleted
n.helpDelete(b, f);
break;
}
//5、 b被标记为删除状态,或者b被标记位删除且b.next是一个marker节点,也只能重试
if (b.value == null || v == n) // b is deleted
break;
//6、 执行到这里:说明n节点就是z节点本身,显然它在已经在4步骤被删除掉,因此可直接返回该n节点,需要注意:返回的n节点只有key不为空,其value是null的。
if ((c = cpr(cmp, key, n.key)) == 0)
return n;
// 说明key原本就不在链表上,可以直接返回。
if (c < 0)
break outer;
// b、n同时后移一步
b = n;
n = f;
}
}
return null;
}

“新插入的z节点被标记删除后,需要对其上方的新建索引节点都一起删除”基于这个原因用findNode方法去完成,那么可以这样提问:能否直接调用findPredecessor 删除逻辑呢?如下所示

findPredecessor是在检索的沿途中会删除那些被标记“删除状态的”索引节点,它只在索引层范围内进行,不会下沉到数据层链表中操作。

而findNode方法可以进入到数据层链表中,把已经标记为删除状态的数据节点node删除,这个功能是findPredecessor不具备的,同时findNode内部调用findPredecessor方法,完成了额外的索引节点删除功能。

1
2
3
4
5
6

if (t.node.value == null) {
// findNode(key)
findPredecessor(key, cmp);
break splice;
}

以下是官方关于findNode方法设计说明及其实现功能:新插入的z节点被标记删除后,需要对其上方的新建索引节点都一起删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
返回一个null(如果给定的key不在数据链表里面)或者一个持有key的node(node的value当然是null),清除沿途遍历到的所有已标记“node.value=null”的数据层节点(注意这里的nodes是数据层链表中的节点,不是指清除索引层节点)
Returns node holding key or null if no such, clearing out any deleted nodes seen along the way.
从key的前驱节点b开始遍历base-level的数据层链表(如有必要则需要重复遍历),向右移动过程中遇到标记位删除的节点就处理它
Repeatedly traverses at base-level looking for key starting at predecessor returned from findPredecessor, processing base-level deletions as encountered.
调用者可以使用findNode方法获得额外收益即沿途过程中能清除被标记为删除状态的数据节点。
Some callers rely on this side-effect of clearing deleted nodes.

出现以下三种情况之一,都会使得执行流再次回到b节点重新遍历
Restarts occur, at traversal step centered on node n,
(1)考察(b,n,f)三个节点,若n节点已发生改变,不再是b.next执行的原n节点,因此本轮遍历无法处理这种删除操作(cannot unlink),需重试
if: (1) After reading n's next field, n is no longer assumed predecessor b's current successor, which means that we don't have a consistent 3-node snapshot and so cannot unlink any subsequent deleted nodes encountered.

(2)考察(b,n,f)三个节点,若n.value=null,说明n被标记为删除状态,这种情况下,先去n.helpDelete(b, f)删除n节点本身,然后重新遍历
(2) n's value field is null, indicating n is deleted, in which case we help out an ongoing structural deletion before retrying. Even though there are cases where such unlinking doesn't require restart, they aren't sorted out here because doing so would not usually outweigh cost of restarting.

(3)考察(b,n,f)三个节点,若n是marker节点(说明b被标记)或者b.value=null,说明findPredecessor方法返回的是一个被标记删除的数据节点,此时无法正确获得predecessor节点就不能继续执行unlink操作。只能重新回到for循环继续调用findPredecessor,找到新的(b,n,f)
(3) n is a marker or n's predecessor's value field is null, indicating (among other possibilities) that findPredecessor returned a deleted node. We can't unlink the node because we don't know its predecessor, so rely on another call to findPredecessor to notice and return some earlier predecessor, which it will do.
第3种check只会在循环的开始才会严格执行检查,但每次迭代都会执行此检查,以帮助调用方避免和其他线程发生竞争,如果调用方cas失败,只能retry。
This check is only strictly needed at beginning of loop, (and the b.value check isn't strictly needed at all) but is done each iteration to help avoid contention with other threads by callers that will fail to be able to change links, and so will retry anyway.

doPut, doRemove, and findNear方法中loop设计都会有以上三种情况的检查逻辑。findFirst、findLast也有类似的检查逻辑
The traversal loops in doPut, doRemove, and findNear all include the same three kinds of checks. And specialized versions appear in findFirst, and findLast and their variants. They can't easily share code because each uses the reads of fields held in locals occurring in the orders they were performed.
Params:
key – the key
Returns:
node holding key, or null if no suc