并发编程从零开始(九)-ConcurrentSkipListMap&Set

本文介绍了并发编程中的并发容器ConcurrentSkipListMap和ConcurrentSkipListSet,重点讨论了ConcurrentSkipListMap的实现原理,包括无锁链表的解决策略、跳查表的数据结构以及put、remove、get操作的分析,同时提到了ConcurrentSkipListSet作为ConcurrentSkipListMap的封装应用。
摘要由CSDN通过智能技术生成

并发编程从零开始(九)-ConcurrentSkipListMap&Set

CAS知识点补充:

我们都知道在使用 CAS 也就是使用 compareAndSet(current,next)方法进行无锁自加或者更换栈的表头之类的问题时会出现ABA问题。

Java中使用 AtomicStampedReference 来解决 CAS 中的ABA问题,它不再像一般原子类中的 compareAndSet 方法一样只比较内存中的值也当前值是否相等,而且先比较引用是否相等,然后比较值是否相等,此外还会比对版本戳是否和预期的值相等,这样就避免了ABA问题。

常用API:

//构造方法, 传入引用和戳
public AtomicStampedReference(V initialRef, int initialStamp)
//返回引用
public V getReference()
//返回版本戳
public int getStamp()
//如果当前引用 等于 预期值并且 当前版本戳等于预期版本戳, 将更新新的引用和新的版本戳到内存
public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp)
//使用方法和compareAndSet相同,但是weakCompareAndSet有可能不是原子的去更新值,这取决于虚拟机的实现。
public boolean weekCompareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp)
    
//如果当前引用 等于 预期引用, 将更新新的版本戳到内存
public boolean attemptStamp(V expectedReference, int newStamp)
//设置当前引用的新引用和版本戳
public void set(V newReference, int newStamp) 

5.6 ConcurrentSkipListMap/Set

ConcurrentHashMap 是一种 key 无序的 HashMap,ConcurrentSkipListMap则是 key 有序的,实现了NavigableMap接口,此接口又继承了SortedMap接口。

5.6.1 ConcurrentSkipListMap

1.为什么要使用kipList实现Map?

在Java的util包中,有一个非线程安全的HashMap,也就是TreeMap,是key有序的,基于红黑树实现。

而在Concurrent包中,提供的key有序的HashMap,也就是ConcurrentSkipListMap,是基于SkipList(跳查表)来实现的。这里为什么不用红黑树,而用跳查表来实现呢?

因为目前计算机领域还未找到一种高效的、作用在树上的、无锁的、增加和删除节点的办法。

那为什么SkipList可以无锁地实现节点的增加、删除呢?这要从无锁链表的实现说起。

2. 无锁链表

在前面讲解AQS时,曾反复用到无锁队列,其实现也是链表。究竟二者的区别在哪呢?

前面讲的无锁队列、栈,都是只在队头、队尾进行CAS操作,通常不会有问题。如果在链表的中间进行插入或删除操作,按照通常的CAS做法,就会出现问题!

关于这个问题,Doug Lea的论文中有清晰的论述,此处引用如下:

操作1:在节点10后面插入节点20。如下图所示,首先把节点20的next指针指向节点30,然后对节点10的next指针执行CAS操作,使其指向节点20即可。

image-20211028111534075

操作2:删除节点10。如下图所示,只需把头节点的next指针,进行CAS操作到节点30即可。

image-20211028111554921

但是,如果两个线程同时操作,一个删除节点10,一个要在节点10后面插入节点20。并且这两个操作都各自是CAS的,此时就会出现问题。如下图所示,删除节点10,会同时把新插入的节点20也删除掉!这个问题超出了CAS的解决范围。

image-20211028111626340

为什么会出现这个问题呢?

究其原因:在删除节点10的时候,实际受到操作的是节点10的前驱,也就是头节点。节点10本身没 有任何变化。故而,再往节点10后插入节点20的线程,并不知道节点10已经被删除了!

针对这个问题,在论文中提出了如下的解决办法,如下图所示,把节点 10 的删除分为两2步:

第一步,把节点10的next指针,mark成删除,即软删除;

第二步,找机会,物理删除。

做标记之后,当线程再往节点10后面插入节点20的时候,便可以先进行判断,节点10是否已经被删 除,从而避免在一个删除的节点10后面插入节点20。这个解决方法有一个关键点:“把节点10的next指针指向节点20(插入操作)”和“判断节点10本身是否已经删除(判断操作),必须是原子的,必须在1 个CAS操作里面完成!

image-20211028112024573

具体的实现有两个办法:

办法一:AtomicMarkableReference

保证每个 next 是 AtomicMarkableReference 类型。但这个办法不够高效,Doug Lea 在ConcurrentSkipListMap的实现中用了另一种办法(即Mark节点方法)。

办法2:Mark节点

我们的目的是标记节点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]。在定位之后,执行下面几个步骤:

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

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

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

3. get分析

image-20211028122355753

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

5.6.2 ConcurrentSkipListSet

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

image-20211028122501355

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值