并发编程从零开始(九)-ConcurrentSkipListMap&Set
并发编程从零开始(九)-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]。在定位之后,执行下面几个步骤:
如果发现b、n已经被删除了,则执行对应的删除清理逻辑;
否则,如果没有找到待删除的(k, v),返回null;
如果找到了待删除的元素,也就是节点n,则把n的value置为null,同时在n的后面加上Marker节点,同时检查是否需要降低Index的层次。
3. get分析
无论是插入、删除,还是查找,都有相似的逻辑,都需要先定位到元素位置[b,n],然后判断b、n是否已经被删除,如果是,则需要执行相应的删除清理逻辑。这也正是无锁链表复杂的地方。
5.6.2 ConcurrentSkipListSet
如下面代码所示,ConcurrentSkipListSet只是对ConcurrentSkipListMap的简单封装,此处不再进一步展开叙述。
并发编程从零开始(九)-ConcurrentSkipListMap&Set的更多相关文章
- 并发编程从零开始(六)-BlockingDeque+CopyOnWrite
并发编程从零开始(六)-BlockingDeque+CopyOnWrite 5.2 BlockingDeque BlockingDeque定义了一个阻塞的双端队列接口: 该接口继承了BlockingQ ...
- 并发编程从零开始(八)-ConcurrentHashMap
并发编程从零开始(八)-ConcurrentHashMap 5.5 ConcurrentHashMap HashMap通常的实现方式是"数组+链表",这种方式被称为"拉链 ...
- 并发编程从零开始(十一)-Atomic类
并发编程从零开始(十一)-Atomic类 7 Atomic类 7.1 AtomicInteger和AtomicLong 如下面代码所示,对于一个整数的加减操作,要保证线程安全,需要加锁,也就是加syn ...
- 并发编程从零开始(十二)-Lock与Condition
并发编程从零开始(十二)-Lock与Condition 8 Lock与Condition 8.1 互斥锁 8.1.1 锁的可重入性 "可重入锁"是指当一个线程调用 object.l ...
- 并发编程从零开始(十四)-Executors工具类
并发编程从零开始(十四)-Executors工具类 12 Executors工具类 concurrent包提供了Executors工具类,利用它可以创建各种不同类型的线程池 12.1 四种对比 单线程 ...
- Java并发编程(九)并发容器
并发容器的简单介绍: ConcurrentHashMap代替同步的Map(Collections.synchronized(new HashMap())),众所周知,HashMap是根据散列值分段存储 ...
- 并发编程(九)—— Java 并发队列 BlockingQueue 实现之 LinkedBlockingQueue 源码分析
LinkedBlockingQueue 在看源码之前,通过查询API发现对LinkedBlockingQueue特点的简单介绍: 1.LinkedBlockingQueue是一个由链表实现的有界队列阻 ...
- Java并发编程(九):拓展
java多线程死锁理解 Java多线程并发最佳实践 Spring与线程安全 HashMap与ConcurrentHashMap 关于java集合类HashMap的理解 , 数据结构之 ...
- Java并发编程(九)安全发布
之前讨论是如何将对象封闭在线程之中,这样可以减少一些并发带来的同步和可见性问题.但是在有些时候,我们希望在多个线程间共享对象,此时必须确保安全地进行共享. [不安全发布的示例] 可见性问题:其他线程看 ...
随机推荐
- Element 对话框简单使用
官方文档介绍的是页内对话框,但没有基于组件的对话框,这里记录一下,原理就是父子传值是否显示 父页导入组件 <template> <div class="home" ...
- JS021. 拦截事件的显式处理与默认动作(Web API: event.preventDefault)
Web API - event.preventDefault( ) Event 接口的 preventDefault( ) 方法,告诉 user agent :如果此事件没有被显式处理,它默认的动作 ...
- zt:我使用过的Linux命令之ar - 创建静态库.a文件
我使用过的Linux命令之ar - 创建静态库.a文件 本文链接:http://codingstandards.iteye.com/blog/1142358 (转载请注明出处) 用途说明 创建静 ...
- Insecure CAPTCHA (不安全的验证码)
dvwa不能正常显示,需要在配置文件中加入谷歌的密钥: $_DVWA[ 'recaptcha_public_key' ] = '6LfX8tQUAAAAAOqhpvS7-b4RQ_9GVQIh48dR ...
- 10.6Java学习
1.类,对象,方法的定义.2.标识符分为两类:关键字/常见的基本类型:boolean(布尔型),byte(字节型),char(字符型),double(双精度),float(浮点),int(整型),lo ...
- HTML音乐悬浮播放器
话不多说先上代码 <link rel="stylesheet" href="http://47.102.203.92/css/APlayer.min.css&quo ...
- Docker系列(1) - Centos8.X安装Docker
环境准备 需要会Linux的基础 Centos8.x 使用Xshell连接远程服务器 环境查看 #系统内核是4.18以上 [root@localhost ~]# uname -r 4.18.0-305 ...
- springboot pom.xml
Demo project for Spring Boot <?xml version="1.0" encoding="UTF-8"?> <pr ...
- Appium WebView控件定位
背景 移动应用可以粗分为三种:原生应用(native app), 网页应用(web app,或HTML5 app),以及它们的混血儿--混合模式移动应用(hybrid app). 什么是Hybrid ...
- 解决Windows 游戏 错误代码 1170000
安装"Xbox标识提供程序" 下载地址:https://www.microsoft.com/store/apps/9wzdncrd1hkw