ConcurrentHashMap实现原理

ConcurrentHashMap是Java1.5中引用的一个线程安全的支持高并发的HashMap集合类。这篇文章总结了ConcurrentHashMap的内部实现原理,是对于自己理解后的一些整理。


1.HashTable与ConcurrentHashMap的对比

HashTable本身是线程安全的,写过Java程序的都知道通过加Synchronized关键字实现线程安全,这样对整张表加锁实现同步的一个缺陷就在于使程序的效率变得很低。这就是为什么Java中会在1.5后引入ConcurrentHashMap的原因。

从图中可以看出,HashTable的锁加在整个Hash表上,而ConcurrentHashMap将锁加在segment上(每个段上),这样我们在对segment1操作的时候,同时也可以对segment2中的数据操作,这样效率就会高很多。

2.ConcurrentHashMap的内部结构

ConcurrentHashMap主要有三大结构:整个Hash表,segment(段),HashEntry(节点)。每个segment就相当于一个HashTable。

(1)HashEntry类

每个HashEntry代表Hash表中的一个节点,在其定义的结构中可以看到,除了value值没有定义final,其余的都定义为final类型,我们知道Java中关键词final修饰的域成为最终域。用关键词final修饰的变量一旦赋值,就不能改变,也称为修饰的标识为常量。这就意味着我们删除或者增加一个节点的时候,就必须从头开始重新建立Hash链,因为next引用值需要改变。

由于这样的特性,所以插入Hash链中的数据都是从头开始插入的。例如将A,B,C插入空桶中,插入后的结构为:

(2)segment类

Segment 类继承于 ReentrantLock 类,从而使得 Segment 对象能充当锁的角色。每个 Segment 对象用来守护其(成员对象 table 中)包含的若干个桶。

table 是一个由 HashEntry 对象组成的数组。table 数组的每一个数组成员就是散列映射表的一个桶。

count 变量是一个计数器,它表示每个 Segment 对象管理的 table 数组(若干个 HashEntry 组成的链表)包含的 HashEntry 对象的个数。每一个 Segment 对象都有一个 count 对象来表示本 Segment 中包含的 HashEntry 对象的总数。注意,之所以在每个 Segment 对象中包含一个计数器,而不是在 ConcurrentHashMap 中使用全局的计数器,是为了避免出现“热点域”而影响 ConcurrentHashMap 的并发性。

ConcurrentHashMap 类

默认的情况下,每个ConcurrentHashMap 类会创建16个并发的segment,每个segment里面包含多个Hash表,每个Hash链都是有HashEntry节点组成的。

3.用分离锁实现多个线程间的并发写操作

(1)Put方法的实现

整个代码通过注释很好理解了,稍微要注意的是这里的加锁是针对具体的segment,而不是对整个ConcurrentHashMap。Put方法从源码上可以看出是从链表的头部插入新的数据的。

(2)Get方法的实现

ConcurrentHashMap中的读方法不需要加锁,所有的修改操作在进行结构修改时都会在最后一步写count 变量,通过这种机制保证get操作能够得到几乎最新的结构更新。

(3)Remove方法的实现

整个操作是在持有段锁的情况下执行的,空白行之前的行主要是定位到要删除的节点e。接下来,如果不存在这个节点就直接返回null,否则就要将e前面的结点复制一遍,尾结点指向e的下一个结点。e后面的结点不需要复制,它们可以重用。

中间那个for循环是做什么用的呢?从代码来看,就是将定位之后的所有entry克隆并拼回前面去,但有必要吗?每次删除一个元素就要将那之前的元素克隆一遍?这点其实是由entry的不变性来决定的,仔细观察entry定义,发现除了value,其他所有属性都是用final来修饰的,这意味着在第一次设置了next域之后便不能再改变它,取而代之的是将它之前的节点全都克隆一次。至于entry为什么要设置为不变性,这跟不变性的访问不需要同步从而节省时间有关。

执行删除之前的原链表:

执行删除之后的新链表

注意:新链表在clone的时候。顺序发生反转,A->B变为B->A。

(4)containsKey方法的实现

containsKey方法操作相对简单,因为它不需要读取值。

4.总结

在使用锁来协调多线程间并发访问的模式下,减小对锁的竞争可以有效提高并发性。有两种方式可以减小对锁的竞争:

  • 减小请求同一个锁的频率。
  • 减少持有锁的时间。

ConcurrentHashMap 的高并发性主要来自于三个方面:

  • 用分离锁实现多个线程间的更深层次的共享访问。
  • 用 HashEntery 对象的不变性来降低执行读操作的线程在遍历链表期间对加锁的需求。
  • 通过对同一个 Volatile 变量的写 / 读访问,协调不同线程间读 / 写操作的内存可见性。

使用分离锁,减小了请求同一个锁的频率。

concurrentHashMap在jdk1.8中主要做了2方面的改进

改进一:取消segments字段,直接采用transient volatile HashEntry<K,V>[] table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率。

改进二:将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构。对于hash表来说,最核心的能力在于将key hash之后能均匀的分布在数组中。如果hash之后散列的很均匀,那么table数组中的每个队列长度主要为0或者1。但实际情况并非总是如此理想,虽然ConcurrentHashMap类默认的加载因子为0.75,但是在数据量过大或者运气不佳的情况下,还是会存在一些队列长度过长的情况,如果还是采用单向列表方式,那么查询某个节点的时间复杂度为O(n);因此,对于个数超过8(默认值)的列表,jdk1.8中采用了红黑树的结构,那么查询的时间复杂度可以降低到O(logN),可以改进性能。

本文综合参考与网络,参考链接如下:

https://www.cnblogs.com/everSeeker/p/5601861.html

http://www.cnblogs.com/ITtangtang/p/3948786.html

原文发布于微信公众号 - Spark学习技巧(bigdatatip)

原文发表时间:2018-04-01

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Golang语言社区

轻量级交互数据json格式初探

什么是 JSON ? JSON 指的是 JavaScript 对象表示法(JavaScript Object Notation) JSON 是轻量级的文本数据交...

3186
来自专栏决胜机器学习

《Redis设计与实现》读书笔记(二) ——Redis中的字典(Hash)

《Redis设计与实现》读书笔记(二) ——Redis中的字典(Hash) (原创内容,转载请注明来源,谢谢) 一、概述 字典,又称符号表、关联数组、映射,是一...

36310
来自专栏猿人谷

技术怪题

有些公司的题真的是会让咱们大跌眼镜。不信,看下面一道题。 int main() { if( )    {  printf("Hello "); }...

1907
来自专栏前端杂货铺

深入seajs源码系列三

入口方法        每个程序都有个入口方法,类似于c的main函数,seajs也不例外。系列一的demo在首页使用了seajs.use(),这便是入口方法。...

2646
来自专栏web前端

JavaScript基础学习--08 JS作用域

Demos:   https://github.com/jiangheyan/JavaScriptBase 一、浏览器      1、“JS解析器”(至少分为两...

1925
来自专栏coder修行路

Nginx location 匹配顺序整理

Nginx location模块整理 具体的Nginx安装就不在这里描述了,这里只是为了对location的描述 Nginx环境 a. 查看当前系统cat /e...

2647
来自专栏python3

python3--模块collections,time,random,sys

有如下值集合[11,22,33,44,55,66,77,88,99,90......],将所有大于66的值保存至字典的第一个key中,小于66的值保存至第二个k...

792
来自专栏鸿的学习笔记

计算机基础小整理

一、CPU 在平时写的程序可以视为数据和指令的组合体,所有的程序都是copy了一份到内存中才能运行,内存地址是指在内存中保存命令和数据的场所,通过地址来标记和指...

782
来自专栏海天一树

小朋友学C语言(42):gets和fgets

这里可以看出来,定义了s的长度为3,但是用gets()输入字符串的时候,并不会去检查字符串的长度,所以导致char的值不是a,而是”uvwxyz”中的第四个字符...

511
来自专栏机器学习从入门到成神

队列和栈面试题(一)— 请编写一个程序,按升序对栈进行排序,要求最多只能使用一个额外的栈存放临时数据

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/sinat_35512245/articl...

942

扫码关注云+社区