计算机程序的思维逻辑 (75) - 并发容器 - 基于SkipList的Map和Set
本系列文章经补充和完善,已修订整理成书《Java编程的逻辑》,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http://item.jd.com/12299018.html
上节我们介绍了ConcurrentHashMap,ConcurrentHashMap不能排序,容器类中可以排序的Map和Set是TreeMap和TreeSet,但它们不是线程安全的。Java并发包中与TreeMap/TreeSet对应的并发版本是ConcurrentSkipListMap和ConcurrentSkipListSet,本节,我们就来简要探讨这两个类。
基本概念
我们知道,TreeSet是基于TreeMap实现的,与此类似,ConcurrentSkipListSet也是基于ConcurrentSkipListMap实现的,所以,我们主要来探讨ConcurrentSkipListMap。
ConcurrentSkipListMap是基于SkipList实现的,SkipList称为跳跃表或跳表,是一种数据结构,待会我们会进一步介绍。并发版本为什么采用跳表而不是树呢?原因也很简单,因为跳表更易于实现高效并发算法。
ConcurrentSkipListMap有如下特点:
- 没有使用锁,所有操作都是无阻塞的,所有操作都可以并行,包括写,多个线程可以同时写。
- 与ConcurrentHashMap类似,迭代器不会抛出ConcurrentModificationException,是弱一致的,迭代可能反映最新修改也可能不反映,一些方法如putAll, clear不是原子的。
- 与ConcurrentHashMap类似,同样实现了ConcurrentMap接口,直接支持一些原子复合操作。
- 与TreeMap一样,可排序,默认按键自然有序,可以传递比较器自定义排序,实现了SortedMap和NavigableMap接口。
看段简单的使用代码:
public static void main(String[] args) {
Map<String, String> map = new ConcurrentSkipListMap<>(
Collections.reverseOrder());
map.put("a", "abstract");
map.put("c", "call");
map.put("b", "basic");
System.out.println(map.toString());
}
程序输出为:
{c=call, b=basic, a=abstract}
表示是有序的。
ConcurrentSkipListMap的大部分方法,我们之前都有介绍过,有序的方法,与TreeMap是类似的,原子复合操作,与ConcurrentHashMap是类似的,所以我们就不赘述了。
需要说明一下的是它的size方法,与大多数容器实现不同,这个方法不是常量操作,它需要遍历所有元素,复杂度为O(N),而且遍历结束后,元素个数可能已经变了,一般而言,在并发应用中,这个方法用处不大。
下面我们主要介绍下其基本实现原理。
基本实现原理
我们先来介绍下跳表的结构,跳表是基于链表的,在链表的基础上加了多层索引结构。我们通过一个简单的例子来看下,假定容器中包含如下元素:
3, 6, 7, 9, 12, 17, 19, 21, 25, 26
对Map来说,这些值可以视为键。ConcurrentSkipListMap会构造类似下图所示的跳表结构:
最下面一层,就是最基本的单向链表,这个链表是有序的。虽然是有序的,但我们知道,与数组不同,链表不能根据索引直接定位,不能进行二分查找。
为了快速查找,跳表有多层索引结构,这个例子中有两层,第一层有5个节点,第二层有2个节点。高层的索引节点一定同时是低层的索引节点,比如9和21。
高层的索引节点少,低层的多,统计概率上,第一层索引节点是实际元素数的1/2,第二层是第一层的1/2,逐层减半,但这不是绝对的,有随机性,只是大概如此。
对于每个索引节点,有两个指针,一个向右,指向下一个同层的索引节点,另一个向下,指向下一层的索引节点或基本链表节点。
有了这个结构,就可以实现类似二分查找了,查找元素总是从最高层开始,将待查值与下一个索引节点的值进行比较,如果大于索引节点,就向右移动,继续比较,如果小于,则向下移动到下一层进行比较。
下图两条线展示了查找值19和8的过程:
对于19,查找过程是:
- 与9相比,大于9
- 向右与21相比,小于21
- 向下与17相比,大于17
- 向右与21相比,小于21
- 向下与19相比,找到
对于8,查找过程是:
- 与9相比,小于9
- 向下与6相比,大于6
- 向右与9相比,小于9
- 向下与7相比,大于7
- 向右与9相比,小于9,不能再向下,没找到
这个结构是有序的,查找的性能与二叉树类似,复杂度是O(log(N)),不过,这个结构是如何构建起来的呢?
与二叉树类似,这个结构是在更新过程中进行保持的,保存元素的基本思路是:
- 先保存到基本链表,找到待插入的位置,找到位置后,先插入基本链表
- 更新索引层。
对于索引更新,随机计算一个数,表示为该元素最高建几层索引,一层的概率为1/2,二层为1/4,三层为1/8,依次类推。然后从最高层到最低层,在每一层,为该元素建立索引节点,建的过程也是先查找位置,再插入。
对于删除元素,ConcurrentSkipListMap不是一下子真的进行删除,为了避免并发冲突,有一个复杂的标记过程,在内部遍历元素的过程中会真正删除。
以上我们只是介绍了基本思路,为了实现并发安全、高效、无锁非阻塞,ConcurrentSkipListMap的实现非常复杂,具体我们就不探讨了,感兴趣的读者可以参考其源码,其中提到了多篇学术论文,论文中描述了它参考的一些算法。
对于常见的操作,如get/put/remove/containsKey,ConcurrentSkipListMap的复杂度都是O(log(N))。
上面介绍的SkipList结构是为了便于并发操作的,如果不需要并发,可以使用另一种更为高效的结构,数据和所有层的索引放到一个节点中,如下图所示:
对于一个元素,只有一个节点,只是每个节点的索引个数可能不同,在新建一个节点时,使用随机算法决定它的索引个数,平均而言,1/2的元素有两个索引,1/4的元素有三个索引,依次类推。
小结
本节简要介绍了ConcurrentSkipListMap和ConcurrentSkipListSet,它们基于跳表实现,有序,无锁非阻塞,完全并行,主要操作复杂度为O(log(N))。
下一节,我们来探讨并发队列。
(与其他章节一样,本节所有代码位于 https://github.com/swiftma/program-logic)
----------------
未完待续,查看最新文章,敬请关注微信公众号“老马说编程”(扫描下方二维码),从入门到高级,深入浅出,老马和你一起探索Java编程及计算机技术的本质。用心原创,保留所有版权。
计算机程序的思维逻辑 (75) - 并发容器 - 基于SkipList的Map和Set的更多相关文章
- Java编程的逻辑 (75) - 并发容器 - 基于SkipList的Map和Set
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...
- 计算机程序的思维逻辑 (73) - 并发容器 - 写时拷贝的List和Set
本节以及接下来的几节,我们探讨Java并发包中的容器类.本节先介绍两个简单的类CopyOnWriteArrayList和CopyOnWriteArraySet,讨论它们的用法和实现原理.它们的用法比较 ...
- 计算机程序的思维逻辑 (74) - 并发容器 - ConcurrentHashMap
本节介绍一个常用的并发容器 - ConcurrentHashMap,它是HashMap的并发版本,与HashMap相比,它有如下特点: 并发安全 直接支持一些原子复合操作 支持高并发.读操作完全并行. ...
- 计算机程序的思维逻辑 (64) - 常见文件类型处理: 属性文件/CSV/EXCEL/HTML/压缩文件
对于处理文件,我们介绍了流的方式,57节介绍了字节流,58节介绍了字符流,同时,也介绍了比较底层的操作文件的方式,60节介绍了随机读写文件,61节介绍了内存映射文件,我们也介绍了对象的序列化/反序列化 ...
- 计算机程序的思维逻辑 (8) - char的真正含义
看似简单的char 通过前两节,我们应该对字符和文本的编码和乱码有了一个清晰的认识,但前两节都是与编程语言无关的,我们还是不知道怎么在程序中处理字符和文本. 本节讨论在Java中进行字符处理的基础 - ...
- 计算机程序的思维逻辑 (29) - 剖析String
上节介绍了单个字符的封装类Character,本节介绍字符串类.字符串操作大概是计算机程序中最常见的操作了,Java中表示字符串的类是String,本节就来详细介绍String. 字符串的基本使用是比 ...
- Java编程的逻辑 (73) - 并发容器 - 写时拷贝的List和Set
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...
- Java编程的逻辑 (74) - 并发容器 - ConcurrentHashMap
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...
- 计算机程序的思维逻辑 (66) - 理解synchronized
上节我们提到了多线程共享内存的两个问题,一个是竞态条件,另一个是内存可见性,我们提到,解决这两个问题的一个方案是使用synchronized关键字,本节就来讨论这个关键字. 用法 synchroniz ...
随机推荐
- Git学习之路(3)-提交文件到三个区
▓▓▓▓▓▓ 大致介绍 年过的差不多了,开始学习!小白学Git(持续更新) Git有三个工作区域: ◆ 工作区(Working Directory) ◆ 暂存区(Stage) ◆ 版本库(Reposi ...
- Vmware 中安装 Ubuntu Server (或者ubuntu 以文本界面登陆时) 分辨率无法全屏问题
Vmware 中安装 Ubuntu Server/Ubuntu 分辨率,无法全屏问题 需要更改grub设置 在终端或者文本界面按下列步骤进行设置: 第一步: 输入命令 sudo vim /etc/de ...
- [TensorFlow] Basic Usage
Install TensorFlow on mac Install pip # Mac OS X $ sudo easy_install pip $ sudo easy_install --upgra ...
- javascript设计模式与开发实践阅读笔记(11)—— 模板方法模式
模板方法模式: 由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类.通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序.子类通过继承这个抽象类,也继 ...
- MongoDB与Redis的比较
MongoDB和Redis都是NoSQL,采用结构型数据存储.二者在使用场景中,存在一定的区别,这也主要由于二者在内存映射的处理过程,持久化的处理方法不同. MongoDB建议集群部署,更多的考虑到集 ...
- HQL连接查询
HQL提供了连接查询机制如内连接,外连接,,还允许显示指定迫切内连接,和迫切外联结. 连接类型 内连接 inner join 或join 迫切内链接 inner join fetch 左外联结 le ...
- Use Prerender to improve AngularJS SEO
Use Prerender to improve AngularJS SEO Nuget Package of ASP.NET MVC HttpModule for prerender.io: Ins ...
- css3 3d初入门(一)
css3 3D.html div.oembedall-githubrepos { border: 1px solid #DDD; list-style-type: none; margin: 0 0 ...
- Kafka 0.10 Producer网络流程简述
1.Producer 网络请求 1.1 Producer Client角度 KafkaProducer主要靠Sender来发送数据给Broker. Sender: 该线程handles the sen ...
- shiro的入门实例-shiro于spring的整合
shiro是一款java安全框架.简单而且可以满足实际的工作需要 第一步.导入maven依赖 <!-- shiro --> <dependency> <groupId&g ...