资讯专栏INFORMATION COLUMN

ConcurrentHashMap学习

mayaohua / 1396人阅读

摘要:与和是一一对应的,对充当锁的角色,每当对数组的数据进行修改时,首先要获取对应的锁解决散列冲突的方式是采用分离链表法分散链表法使用链表解决冲突,将散列值相同的元素都保存到一个链表中。负载因子,默认为。

一、为什么要用ConcurrentHashMap?

1、HashMap线程不安全,并且进行put操作会导致死循环(由于HashMap的Entry链表形成环形数据结构,Entry下的next节点永远不为空)
2、HashTable多线程效率低下,主要表现在数据操作方法头采用synchronized互斥锁,使得其他线程访问同步方法会进入阻塞或者是轮询状态(就是线程会无限循环查看锁是否被释放)
3、ConcurrentHashMap的锁分段技术可提升并发访问效率(表现在吞吐量)

二、结构

解析:Segment是一种可重入锁,在ConcurrentHashMap中扮演锁的角色,HashEntry则是用于存储键值对数据。ConcurrentHashMap与Segment和HashEntry是一一对应的,Segment对HashEntry充当锁的角色,每当对HashEntry数组的数据进行修改时,首先要获取对应的Segment锁
解决散列冲突的方式是采用分离链表法:
分散链表法使用链表解决冲突,将散列值相同的元素都保存到一个链表中。当查询的时候,首先找到元素所在的链表,然后遍历链表查找对应的元素,也就是会根据key定位到链表的某个位置,所以不管是hashMap还是concurrentHashMap相同key是只取最后插入的值

图片来自 http://faculty.cs.niu.edu/~fr...

三、初始化

capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍,默认为16,即默认允许16个线程同时访问。
loadFactor:负载因子,默认为 0.75。
threshold:扩容的阈值,等于 capacity * loadFactor
初始化Segment数组,该长度ssize是通过concurrencyLevel计算得出,需要能通过按位与的散列算法来定位数组的索引,故必须要保证Segment的长度为2的N次方,故concurrencyLevel为14、15、16,ssize都等于15,即容器锁的格式也是16
初始化segment,initialCapacity为ConcurrentHashMap初始化容量,loadfactor是每个segment的负载因子

四、定位Segment

由于ConcurrentHashMap使用分段锁Segment来保护不同段的数据,那么在插入和读取元素的时候,必须先通过Wang/Jenkins hash的变种算法对元素的hashCode进行再次散列
那么为什么需要再次散列呢?
为了减少散列冲突,使得元素能够均匀分布在不同的segment中,从而达到提高容器的存取效率,那么不再次散列会出现什么结果?
会导致不同数值它的散列值可能会相同,从而导致所有数值都存在同一个segment中,从而导致segment失去分段锁的意义并且存取缓慢
如果进行再次散列,那么所有的数值都会散列开,之前的问题迎刃而解

五、get、put、size操作

ConcurrentHashMap高效的原因之一在于get操作不需要加锁,因为它所有共享变量都定义成volatile,能够保证在多线程之间的可见性,能够被多线程同时读,并且保证不会读到过期值,并且由volatile修饰的共享变量均不需要回写主内存
put操作时由于对共享变量进行了写的操作,故必须加锁,那么put方法首先会定位到segment(因为segment相当于装载元素的桶),然后进行插入操作,操作分为2步:
1、在插入元素之前会先判断是否需要扩容(所有容器都是需要判断扩容),做法是如果capacity *loadFactor大于threshold则扩容,
2、扩容多少呢?首先会创建一个容量为原来2倍的数组,将原有元素进行散列后插入新的数组,为了高效,只会针对某个segment进行扩容
那这个就是负载因子的作用,这个操作和HashMap不一致,HashMap是在插入完之后进行判断是否需要扩容,就会导致如果下次没有插入数据,那么这个空间就浪费了,在jdk1.8还做了部分修改
size操作:就是为了统计segment的所有大小,那么segment的count是个全局变量是用volatile修饰,最快速的做法是,把所有的segment的count加起来,但是如果在这期间出现了修改操作,那么count的统计就不精确了,如果把segment的put、remove、clean全部锁住,那么效率太低,目前的做法是,尝试2次不锁住segment来统计count,如果统计过程中出现容器变化,那么就加锁,来统计,ConcurrentHashMap是如何知道统计过程中容器发生了变化,用modCount变量,在put、remove、clean方法里操作元素前会将modCount进行加1,比较前后变化

jdk1.8不再采用segment作为锁的粒度,而是直接将HashEntry作为锁,从而降低锁的粒度、
jdk1.8使用内置锁synchronized来代替重入锁ReentrantLock
因为粒度降低了,在相对而言的低粒度加锁方式,synchronized并不比ReentrantLock差,在粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了
JVM的开发团队从来都没有放弃synchronized,而且基于JVM的synchronized优化空间更大,使用内嵌的关键字比使用API更加自然
在大量的数据操作下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存,虽然不是瓶颈,但是也是一个选择依据

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/77470.html

相关文章

  • [学习笔记-Java集合-7] Map - ConcurrentHashMap 源码分析(一)

    摘要:简介是的线程安全版本,内部也是使用数组链表红黑树的结构来存储元素。相比于同样线程安全的来说,效率等各方面都有极大地提高。中的关键字,内部实现为监视器锁,主要是通过对象监视器在对象头中的字段来表明的。 简介 ConcurrentHashMap是HashMap的线程安全版本,内部也是使用(数组 + 链表 + 红黑树)的结构来存储元素。 相比于同样线程安全的HashTable来说,效率等各方...

    SoapEye 评论0 收藏0
  • [学习笔记-Java集合-8] Map - ConcurrentHashMap 源码分析(二)

    摘要:那么,如果之后不是简单的操作,而是还有其它业务操作,之后才是,比如下面这样,这该怎么办呢其它业务操作这时候就没办法使用提供的方法了,只能业务自己来保证线程安全了,比如下面这样其它业务操作这样虽然不太友好,但是最起码能保证业务逻辑是正确的。 删除元素 删除元素跟添加元素一样,都是先找到元素所在的桶,然后采用分段锁的思想锁住整个桶,再进行操作。 public V remove(Objec...

    2501207950 评论0 收藏0
  • 趣谈ConcurrentHashMap

    摘要:最近准备面试,一谈到基础,大部分面试官上来就数据结构素质三连与区别,底层数据结构,为什么能保证线程安全。数组顺序存储,内存连续,查询快,插入删除效率稍微低,不过现在略有改善。而在开始,是由和的方式去实现高并发下的线程安全。 最近准备面试,一谈到java基础,大部分面试官上来就java数据结构素质三连:ArrayList与LinkedList区别,HashMap底层数据结构,Concur...

    Travis 评论0 收藏0
  • ConcurrentHashMap基于JDK1.8源码剖析

    摘要:下面我来简单总结一下的核心要点底层结构是散列表数组链表红黑树,这一点和是一样的。是将所有的方法进行同步,效率低下。而作为一个高并发的容器,它是通过部分锁定算法来进行实现线程安全的。 前言 声明,本文用的是jdk1.8 前面章节回顾: Collection总览 List集合就这么简单【源码剖析】 Map集合、散列表、红黑树介绍 HashMap就是这么简单【源码剖析】 LinkedHas...

    sanyang 评论0 收藏0
  • ConcurrentHashMap中tabAt、setTabAt方法的意义所在

    摘要:总结中针对数组的访问和赋值的意义应该是在于越过对数组操作的包装,进而达到优化性能的目的。以上为抛砖引玉。。参考链接知乎请问数组的行为是如何实现的 在学习ConcurrentHashMap时发现,源码中对table数组的元素进行操作时,使用了三个封装好的原子操作方法,如下: /* ---------------- Table element access -------------- *...

    GeekGhc 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<