并发编程从零开始(九)-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即可。

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

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

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

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

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

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

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

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

具体的实现有两个办法:

办法一: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中的代码略有差异,但不影响下面的原理分析)。

整体结构:

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

上层index:

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

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

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

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

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

1. put实现分析

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

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

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

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

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

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

2. remove(...)分析

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

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

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

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

3. get分析

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

5.6.2 ConcurrentSkipListSet

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

并发编程从零开始(九)-ConcurrentSkipListMap&Set的更多相关文章

  1. 并发编程从零开始(六)-BlockingDeque+CopyOnWrite

    并发编程从零开始(六)-BlockingDeque+CopyOnWrite 5.2 BlockingDeque BlockingDeque定义了一个阻塞的双端队列接口: 该接口继承了BlockingQ ...

  2. 并发编程从零开始(八)-ConcurrentHashMap

    并发编程从零开始(八)-ConcurrentHashMap 5.5 ConcurrentHashMap HashMap通常的实现方式是"数组+链表",这种方式被称为"拉链 ...

  3. 并发编程从零开始(十一)-Atomic类

    并发编程从零开始(十一)-Atomic类 7 Atomic类 7.1 AtomicInteger和AtomicLong 如下面代码所示,对于一个整数的加减操作,要保证线程安全,需要加锁,也就是加syn ...

  4. 并发编程从零开始(十二)-Lock与Condition

    并发编程从零开始(十二)-Lock与Condition 8 Lock与Condition 8.1 互斥锁 8.1.1 锁的可重入性 "可重入锁"是指当一个线程调用 object.l ...

  5. 并发编程从零开始(十四)-Executors工具类

    并发编程从零开始(十四)-Executors工具类 12 Executors工具类 concurrent包提供了Executors工具类,利用它可以创建各种不同类型的线程池 12.1 四种对比 单线程 ...

  6. Java并发编程(九)并发容器

    并发容器的简单介绍: ConcurrentHashMap代替同步的Map(Collections.synchronized(new HashMap())),众所周知,HashMap是根据散列值分段存储 ...

  7. 并发编程(九)—— Java 并发队列 BlockingQueue 实现之 LinkedBlockingQueue 源码分析

    LinkedBlockingQueue 在看源码之前,通过查询API发现对LinkedBlockingQueue特点的简单介绍: 1.LinkedBlockingQueue是一个由链表实现的有界队列阻 ...

  8. Java并发编程(九):拓展

    java多线程死锁理解 Java多线程并发最佳实践 Spring与线程安全 HashMap与ConcurrentHashMap 关于java集合类HashMap的理解     ,      数据结构之 ...

  9. Java并发编程(九)安全发布

    之前讨论是如何将对象封闭在线程之中,这样可以减少一些并发带来的同步和可见性问题.但是在有些时候,我们希望在多个线程间共享对象,此时必须确保安全地进行共享. [不安全发布的示例] 可见性问题:其他线程看 ...

随机推荐

  1. aes加解密前后端-前端

    一.ajax请求前端 f12请求和响应参数效果: 1.在前端封装ajax的公共Util的js中,封装ajax请求的地方,在beforeSend方法和成功之后的回调函数success方法中: var p ...

  2. 通过HttpURLConnection下载图片到本地--下载附件

    一.背景说明 现在我做的系统中,需要有一个下载附件的功能,其实就是下载图片到本地中.相应的图片保存在多媒体系统中,我们只能拿到它的资源地址(url),而不是真实的文件. 这里记录的是下载单个图片.下篇 ...

  3. Vue中使用 iview 之-踩坑日记

    导航列表: 一.iview单选框Select验证问题 二.iview表单v-if引起的问题 三.Upload 手动上传组件 使用是出现的问题 四.Tabs嵌套使用时的问题 五.Tooltip 换行问题 ...

  4. clion结合vcpkg以及GTest的使用

    目录 一.vcpkg简介.下载和使用 1. vcpkg是什么 2. vcpkg下载 3. 使用vcpkg下载第三方库 二.clion结合vcpkg 1. 方法一:使用环境变量 2. 方法二:添加cma ...

  5. MobaXterm - 渗透之旅的终端神器

    一.背景 1.SSH概念 如果想要连接Linux服务器来进行文件之间的传送,那就需要一个Secure Shell软件(简称SSH的)来完成.从概念上来讲,SSH其实是一个网络协议,允许通过网络连接到L ...

  6. CodeForce-810B Summer sell-off (结构体排序)

    http://codeforces.com/problemset/problem/810/B 已知n天里,已知第i天的供货量和需求量,给定一个f,可以在n天之中选f天促销使得供货量翻倍. 问选择其中f ...

  7. java.lang.SecurityException: MODE_WORLD_READABLE错误解决

    问题描述:运行Android项目有以下报错: 解决方法: 把 MODE_WORLD_READABLE 更换成 MODE_PRIVATE 即可,因为MODE_WORLD_READABLE 模式已经被废弃 ...

  8. Shell系列(18)- 什么是正则表达式

    概念: 正则表达式是用于描述字符排列和匹配模式的一种语法 它主要用于字符串的模式分割.匹配.查找及替换操作.

  9. centos7 .net core 使用supervisor守护进程后台运行

    安装supervisor yum install supervisor 配置supervisor vi /etc/supervisord.conf 拉到最后,这里的意思是 /etc/superviso ...

  10. Python turtle.right与turtle.setheading的区别

    一.概念 turtle.right与turtle.left用法一致,我们以turtle.right为例进行讲述. turtle.right(angle)向右旋转angle角度. turtle.seth ...