JUC源码解析-ConcurrentSkipListMap

本文深入解析了ConcurrentSkipListMap的内部实现,包括其基于跳表的数据结构,节点类型(Node、Index、HeadIndex)以及并发下的插入、删除操作。文章指出,ConcurrentSkipListMap通过使用volatile和CAS保证线程安全,同时利用空值节点和特殊标记节点来处理并发删除,确保了并发性能。文章还探讨了不同场景下的并发协调策略,确保了数据的正确性和一致性。
摘要由CSDN通过智能技术生成

单线程下若想使用有序的键值对,我们选用 TreeMap,若是考虑到线程安全问题,则可以使用 ConcurrentSkipListMap,它与ConcurrentSkipListSet之间的关系就如 TreeMap 与 TreeSet 之家的关系一样,set 由 map来实现,本篇主要来分析 ConcurrentSkipListMap。

底层数据结构是 跳表 Skip List:

  • 跳表分为多层,层级越高跳跃性越大,数据越少
  • 跳表的层级是通过“掷硬币”方式来决定增长的,也就是说在增加元素时,概率增长层数
  • 查找数据时,按照从上到下,从左往右的顺序查找
  • 时间复杂度O(log n),空间复杂度O(n)

跳表的实现:
在这里插入图片描述
三种类型的节点:
Node:键值对存储在其中,next字段构成了单向链表

Index:跳表中的索引节点,包含了右指针(right),向下的索引(down)和持有一个Node对象的引用。right 指向右侧的Index,down 指向下层的Index。

HeadIndex:继承自Index,每一条从左至右的Index 链的头节点,多了一个标识当前层级的level。

跳表结构模型:下层是Node 的单向链表,其上引用其的 Index 节点链,利用 Index 节点的 right 与 down 构成一个二维 Index 链,并非每个Node上都有Index 链,新的 Node 节点是否要构建 Index 链以及构建几层都是堆积的,沿着 Index 链从左至右,从上至下的搜寻过程就是所谓的

初始化时结构如下:
在这里插入图片描述
BASE_HEADER是个空对象,是 Node 链的头节点,其上是 HeadIndex 链,每一个HeadIndex 都是该层 Index 链的头节点;

private static final Object BASE_HEADER = new Object();

ConcurrentSkipListMap#head 指向 HeadIndex 链的顶层

private transient volatile HeadIndex<K,V> head;

HeadIndex 有个 level 字段,用于标识当前层级。

整个跳表结构如下图:
在这里插入图片描述
首先从 head 开始,沿着Index节点链从左至右,从上至下进行搜索,找到合适位置插入,新Node节点是否要在其上创建Index链,要创建几层Index,是随机的,下面分析源码时再说。

跳表结构分为两种链,一个是 Index 二维链表,另一个是 Node 数据链。

在Index 二维链中,每次只需遍历几个Index节点就可找到要 get / put / remove 位置附近的前置 Node 节点,然后从该前置节点处往后遍历Node链找寻,通常也只需较少的遍历就可找到。

并发下 Index 节点的插入与删除:

Index 的 down , node 字段是 final 的,不会改变,不存在安全问题。

Index 节点的 next 指针是 volatile 的,使用 CAS 添加/删除 Index 节点,这保证了安全性,对于失败的线程会采用重试的方式,让其再从头开始遍历搜寻,这是种退让的处理方式,过度的竞争反而影响性能。CAS + volatile 确实保证了单个节点操作的安全性,但是对于整个链在并发下的正确性还需要其它的保护措施:空值节点禁止执行 link / unlink 操作,对于失败的重试机制,以及主动的避让。我会在接下来对它们进行分析。

节点的 Index 链无论插入还是删除都不是一下全部完成,这就造成 Index 二维链表不能即时更新,但这并不会影响安全性,因为我们仍然可以靠着原来的 Index 链来查找,会对性能造成影响。

来看看 Index 节点的连接与断开操作 :link , unlink

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

首先将 newSucc 指向 succ,之后 CAS 将当前Index指向 newSucc,这样一次CAS 就能完成插入操作。竞争导致失败的话,调用方会采取重试的方式来处理。

如果当前 Index 持有的 Node 节点被删除 (指的是其value为null,这代表被删除状态,虽然仍可以访问到)是无法调用 link 方法的,因为这有丢失 Index 节点的风险。在文章后面的论述里,对此做了详细解释。

unlink

        final boolean unlink(Index<K,V> succ) {
   
            return node.value != null && casRight(succ, succ.right);
        }

不允许删除节点的 Index 执行unlink操作,为了防止情况的复杂性。
能调用unlink说明该Index关联的Node之前并未被删除且其next节点是删除状态的节点,因此调用unlink解绑,不过在调用前该节点也被删除了,处理方法就是使其unlink失败,强制让其重试,重试意味着从Index 链头开始重新搜寻。这是为了防止情况的复杂性。

可以看出 Index 的链接/断开都与 Node 节点删除操作的设计息息相关。

关于 Node 链的删除操作:

有些并发下列表的删除操作是在删除时标记已删除节点的 next 指针,以避免与并发插入冲突,以及在遍历时跟踪三元组(前置、节点、后续)以检测何时以及如何取消这些已删除节点的链接。出于性能和空间的考虑ConcurrentSkipListMap没采用这种方式。

ConcurrentSkipListMap 使用 volatile 的 next 指针,并使用 CAS 更改它们。删除时,它们会被更改指向一个新节点上,该节点可以看作是种 “ 标记 ” (该特殊的节点应该具有某种特殊的标识,比如该类中标记节点的key为null,value指向自身)

除了使用删除标记外,列表还使用节点 value 字段的空性(null)来指示删除,延迟删除方案。如果一个节点的 value 为 null,那么我们就认为它已被删除,即使它仍然可以被访问到。

为什么要在要删除的节点后CAS添加一个特殊的标记节点?value为null不就已经可以标识节点的状态是删除了吗?
重点在于CAS,通过主动的CAS来式该节点的其它CAS操作失败,或因竞争或因next改变,总之为删除节点尽量排除其它操作的干扰,那些失败的线程会重试。

以下是在 Node 节点链里删除 n 节点,其前置节点是b,后续节点是f,最初是:

 *        +------+       +------+      +------+
 *   ...  |   b  |------>|   n  |----->|   f  | ...
 *        +------+       +------+      +------+

1,CAS 将 n 的 value 从非空 置为 null
之后遍历到该节点的其它操作将视其为已被删除而做相应处理。其他正在进行的插入和删除操作可能仍然会修改 n 的 next 指针,因为其仍在链内。

2,CAS 将 n 的 next 指向标记节点 marker ( 该节点的key为null,value指向自身) 。之后,对于想将节点插入到 n 后的操作,在检测到 n.next 也就是 marker 后,不会进行插入操作。

 *        +------+       +------+      +------+       +------+
 *   ...  |   b  |------>|   n  |----->|marker|------>|   f  | ...
 *        +------+       +------+      +------+       +------+

3,CAS 将 b 的 next 指向 f 。此后不会在链表中遍历到 n,它最终可以被 gc 掉。

 *        +------+                                    +------+
 *   ...  |   b  |----------------------------------->|   f  | ...
 *        +------+                                    +------+

对于上面的这三步:

对于步骤 1 来说,若是由于与另一个操作的竞争而失败,重试即可。

步骤2-3可能会失败,一些操作在遍历时发现空值节点会帮助进行删除操作,所以存在竞争,采用这种帮助删除的方式而非等待,是为了防止操作因删除节点的影响而迟迟得不到执行,所以主动帮忙执行删除任务。

标记节点的使用稍微使帮助代码变得复杂,因为遍历必须跟踪四个节点(b、n、marker、f)的一致读取,而不仅仅是(b、n、f),不过标记节点 marker 的 next 字段是不会被改变的,空值节点指向 marker 的 next 也是不会再被更改的。这是因为竞争失败的会重试,重试发现空值节点会 helpDelete,帮完忙后会再次重试,所以说 n.next 和 marker.next 是不变的。

(b, n, f) 是三个Node节点,分别表示 (前置节点,当前节点,后置节点),称他们为跟踪三元组。

源码解析

并发下的情况是复杂的,状态不断的变化,通常执行一操作,会先获取此刻状态的一个快照,然后进行分析处理,只要在最后将结果进行正确的同步即可确保程序的线程安全,若是提交结果失败重试即可,重试获取最新的状态再次进行分析处理。

最重要的就是如何正确同步代码,接下来我们来看看ConcurrentSkipListMap是如何做的。

无论get,set ,remove 等都需要先沿着Index 二维链表从左到右从上到下找到相应位置附近的节点,这就是 findPredecessor 主要做的事,方法返回一个Node,之后就从该节点处开始遍历 Node 链,有了在Index二维链上的 跳 ,就避免了从头开始一个个遍历Node 链,也就节省了搜索时间。

findPredecessor

    private Node<K,V> findPredecessor(Object key, Comparator<? super K> cmp) {
   
        if (key == null)
            throw new NullPointerException(); // don't postpone errors
        // 两层for循环
        for (;;) {
   
            for (Index<K,V> q = head, r = q.right, d;;) {
   
                if (r != null) {
   
                    Node<K,V> n = r.node;
                    K k = n.key;
                    // 发现被删除的Node,则unlink修正链
                    // 若失败,break跳出当前循环,而外围是个无限的for循环,
                    // 会重新读取head 开始搜索。
                    if (n.value == null) {
   
                        if (!q.unlink(r))
                            break;           // restart
                        r = q.right;         // reread r
                        continue;
                    }
                    // 继续往右遍历Index链
                    if (cpr(cmp, key, k) > 0) {
   
                        q = r;
                        r = r.right;
                        continue;
                    }
                }
                // 什么情况代表在Index二维链中找到了那个最靠近(从左至右的)的节点?
                // 那就是搜到了某一Node上的Index节点链的最底层
                if ((d = q
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值