ConcurrentHashMap简介

为什么要用ConcurrentHashMap

HashMap是线程不安全的,JDK1.7并发操作可能会出现死链,JDK1.8可能会丢失数据。

避免HashMap的线程安全问题有很多办法,比如改用HashTable或者Collections,synchronizedMap。但是这两者都有共通的问题就是性能很差,不管读写都会给整个集合加锁,导致同一时间的其他操作为阻塞。

JDK1.7

ConcurrentHashMap的构成

ConcurrentHashMap是由多个Segment组成的,Segment本身就相当于一个HashMap对象。同HashMap一样,Segment包含一个HashEntry数组,数组中的每一个HashEntry既是一个键值对,也是一个链表的头节点。
ConcurrentHashMap优势就是采用了[锁分段技术],每一个Segment就好比一个自治区,读写操作高度自治,Segment之间互不影响。ConcurrentHashMap当中每个Segment各自持有一把锁。在保证线程安全的同时降低了锁的粒度,让并发操作效率更高。
在这里插入图片描述

在这里插入图片描述

这样的Segment对象,在ConcurrentHashMap集合中有2的N次方个,共同保存在一个名为segments的数组当中。

整个ConcurrentHashMap的结构如下:

在这里插入图片描述
ConcurrentHashMap是一个二级哈希表。在一个总的哈希表下面,有若干个子哈希表。

这样的二级结构,和数据库的水平拆分有些相似。

  • 水平分割:
    按记录进分分割,不同的记录可以分开保存,每个子表的列数相同。
  • 垂直分割:
    按列进行分割,即把一条记录分开多个地方保存,每个子表的行数相同。

ConcurrentHashMap并发读写情形

1.不同Segment的并发写入

在这里插入图片描述
不同Segment的写入是可以并发执行的。

2.同一Segment的一写一读

在这里插入图片描述
同一Segment的写和读是可以并发执行的。

3.同一Segment的并发写入

在这里插入图片描述
Segment的写入是需要上锁的,因此对同一Segment的并发写入会被阻塞。

Put()方法与Get()方法解析

Get方法:

1.为输入的Key做Hash运算,得到hash值。

2.通过hash值,定位到对应的Segment对象

3.再次通过hash值,定位到Segment当中数组的具体位置。

Put方法:

1.为输入的Key做Hash运算,得到hash值。

2.通过hash值,定位到对应的Segment对象

3.获取可重入锁(ReentrantLock)

4.再次通过hash值,定位到Segment当中数组的具体位置。

5.插入或覆盖HashEntry对象。

6.释放锁。

size()方法解析

Size方法的目的是统计ConcurrentHashMap的总元素数量, 自然需要把各个Segment内部的元素数量汇总起来。
但是,如果在统计Segment元素数量的过程中,已统计过的Segment瞬间插入新的元素,这时候该怎么办呢?

ConcurrentHashMap的Size方法是一个嵌套循环,大体逻辑如下:

1.遍历所有的Segment。

2.把Segment的元素数量累加起来。

3.把Segment的修改次数累加起来。

4.判断所有Segment的总修改次数是否大于上一次的总修改次数。如果大于,说明统计过程中有修改,重新统计,尝试次数+1;如果不是。说明没有修改,统计结束。(cas思想乐观锁)

5.如果尝试次数超过阈值,则对每一个Segment加锁,再重新统计。(悲观锁)

6.再次判断所有Segment的总修改次数是否大于上一次的总修改次数。由于已经加锁,次数一定和上次相等。

7.释放锁,统计结束。

扩容 rehash

当数组元素达到负载因子*数组长度的时候就会触发扩容。
ConcurrentHashMap 的扩容只会扩容到原来的两倍。老数组里的数据移动到新的数组时,位置要么不变,要么变为 index+ oldSize,参数里的 node 会在扩容之后使用链表头插法插入到指定位置。

JDK1.8

ConcurrentHashMap的构成

使用数组+链表+红黑树来实现,利用 CAS + synchronized 来保证并发更新的安全

在这里插入图片描述

ConcurrentHashMap并发读写情形

Put()方法与Get()方法解析

put方法

  • 1.先传入一个k和v的键值对,不可为空(HashMap是可以为空的),如果为空就直接报错。
  • 2.接着去判断table是否为空,如果为空就进入初始化阶段。
  • 3.通过hash计算判断数组中k对应的桶如果为空,那就直接CAS把键值对插入到这个桶中作为头节点,如果成功就break,失败就进入下一次循环。
  • 4.如果这个要插入的桶中的hash值为-1,也就是MOVED状态(也就是这个节点是forwordingNode),那就是说明有线程正在进行扩容操作,那么当前线程就进入协助扩容阶段。
  • 5.如果都不满足,则利用synchronized 锁住头结点开始写入数据,如果这个节点是一个链表节点,那么就遍历这个链表,如果发现有相同的key值就更新value值,如果遍历完了都没有发现相同的key值,就需要在链表的尾部插入该数据。插入结束之后判断该链表节点个数是否大于8,如果大于就需要把链表转化为红黑树存储。
  • 6.如果这个节点是一个红黑树节点,那就需要按照树的插入规则进行插入。
  • 7.在put操作结束后,会调用addCount,更新计数。在并发情况下,如果CAS修改baseCount失败后,就会使用到CounterCell类,新建 counterCells,向其中的一个 cell 累加计数,counterCells 初始有两个 cell,如果计数竞争比较激烈,会创建新的 cell 来累加计数。

get方法

  • 1.根据 hash 值计算位置。
  • 2.查找到指定位置,如果头节点就是要找的,直接返回它的 value.
  • 3.如果头节点 hash 值小于 0 ,说明正在扩容或者是红黑树,查找之。
  • 4.如果是链表,遍历查找之。

get操作可以无锁是由于Node的元素val和指针next是用volatile修饰的,在多线程环境下线程A修改结点的val或者新增节点的时候是对线程B可见的。
在这里插入图片描述

size()方法解析

元素个数保存在 baseCount 中,并发时的个数变动保存在 CounterCell[] 当中。最后统计数量时累加即可。

什么时候会扩容?

当往hashMap中成功插入一个key/value节点时,有两种情况可能触发扩容动作:
1、如果新增节点之后,所在链表的元素个数达到了阈值 8,则会调用treeifyBin方法把链表转换成红黑树,不过在结构转换之前,会对数组长度进行判断,实现如下:如果数组长度n小于阈值MIN_TREEIFY_CAPACITY,默认是64,则会调用tryPresize方法把数组长度扩大到原来的两倍,并触发transfer方法,重新调整节点的位置。

2、调用put方法新增节点时,在结尾会调用addCount方法记录元素个数,并检查是否需要进行扩容,当数组元素个数达到阈值时,会触发transfer方法,重新调整节点的位置。

扩容状态下其他线程对集合进行插入、修改、删除、合并、compute 等操作时遇到 ForwardingNode 节点当前线程就进入协助扩容阶段。

putAll 批量插入或者插入节点后发现存在链表长度达到 8 个或以上,但数组长度为 64 以下时会触发扩容 。

注意:桶上链表长度达到 8 个或者以上,并且数组长度为 64 以下时只会触发扩容而不会将链表转为红黑树 。

ConcurrentHashMap的键值对为什么不能为null,而HashMap却可以?

JDK1.8与1.7的区别

  • JDK1.8版降低了锁的颗粒度,锁的颗粒度就是HashEntry(首节点),发生hash冲突的时候只锁链表或红黑树的首节点,JDK1.7版本锁的颗粒度是基于Segment的,包含多个HashEntry
  • JDK1.8版本发生hash冲突的时候用的是synchronized来进行同步,JDK1.7版本发生hash冲突采用的是ReentrantLock
  • JDK1.8使用红黑树来优化链表(链表大于等于8,数组长度大于64会转换为红黑树),基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档

JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock?

1.因为粒度降低了,在相对而言的低粒度加锁方式,synchronized并不比ReentrantLock差,在粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了

2.JVM的开发团队从来都没有放弃synchronized,而且基于JVM的synchronized优化空间更大,使用内嵌的关键字比使用API更加自然,在大量的数据操作下,对于JVM的内存压力,基于jdk的ReentrantLock会开销更多的内存,虽然不是瓶颈,但是也是一个选择依据

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值