并发编程从零开始(九)-ConcurrentSkipListMapSet (2)

我们的目的是标记节点10已经删除,也就是标记它的next字段。那么可以新造一个marker节点,使节点10的next指针指向该Marker节点。这样,当向节点10的后面插入节点20的时候,就可以在插入的同时判断节点10的next指针是否指向了一个Marker节点,这两个操作可以在一个CAS操作里面完成。

3. 跳查表

解决了无锁链表的插入或删除问题,也就解决了跳查表的一个关键问题。因为跳查表就是多层链表叠起来的。

下面先看一下跳查表的数据结构(下面所用代码都引用自JDK 7,JDK 8中的代码略有差异,但不影响下面的原理分析)。

整体结构:

image-20211028112854596

Node:跳查表底层节点类型。所有的<K, V>对都是由这个单向链表串起来的。

image-20211028112825068

上层index:

image-20211028114051220

node属性不存储实际数据,指向Node节点。

down属性:每个Index节点,必须有一个指针,指向其下一个Level对应的节点。

right属性:Index也组成单向链表。

整个ConcurrentSkipListMap就只需要记录顶层的head节点即可:

image-20211028114149001

下面详细分析如何从跳查表上查找、插入和删除元素:

1. put实现分析

image-20211028114453295

image-20211028114715762

在底层,节点按照从小到大的顺序排列,上面的index层间隔地串在一起,因为从小到大排列。查找的时候,从顶层index开始,自左往右、自上往下,形成图示的遍历曲线。假设要查找的元素是32,遍历过程如下:

先遍历第2层Index,发现在21的后面;

从21下降到第1层Index,从21往后遍历,发现在21和35之间;

从21下降到底层,从21往后遍历,最终发现在29和35之间。

在整个的查找过程中,范围不断缩小,最终定位到底层的两个元素之间。

image-20211028114935460

关于上面的put(...)方法,有一个关键点需要说明:在通过findPredecessor找到了待插入的元素在[b,n]之间之后,并不能马上插入。因为其他线程也在操作这个链表,b、n都有可能被删除,所以在插入之前执行了一系列的检查逻辑,而这也正是无锁链表的复杂之处。

2. remove(...)分析

image-20211028115747143

上面的删除方法和插入方法的逻辑非常类似,因为无论是插入,还是删除,都要先找到元素的前驱,也就是定位到元素所在的区间[b,n]。在定位之后,执行下面几个步骤:

如果发现b、n已经被删除了,则执行对应的删除清理逻辑;

否则,如果没有找到待删除的(k, v),返回null;

如果找到了待删除的元素,也就是节点n,则把n的value置为null,同时在n的后面加上Marker节点,同时检查是否需要降低Index的层次。

3. get分析

image-20211028122355753

无论是插入、删除,还是查找,都有相似的逻辑,都需要先定位到元素位置[b,n],然后判断b、n是否已经被删除,如果是,则需要执行相应的删除清理逻辑。这也正是无锁链表复杂的地方。

5.6.2 ConcurrentSkipListSet

如下面代码所示,ConcurrentSkipListSet只是对ConcurrentSkipListMap的简单封装,此处不再进一步展开叙述。

image-20211028122501355

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zgjygj.html