聊聊ConcurrentHashMap

聊聊ConcurrentHashMap

一、为什么需要ConcurrentHashMap

1、HashMap 线程不安全

在多线程环境下,使用HashMap进行 put 操作的时候可能造成死循环,导致 CPU 使用率太高

为什么HashMap线程不安全?

在 put 的时候,插入元素超过了容量,就会进行rehash,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,存在同时其他的元素也在进行 put 操作,如果 hash 值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在 get 时会出现死循环,所以不安全

2、HashTable 安全但是效率太低了

因为 HashTable 是利用 synchronized 来保证线程安全的,在线程竞争激烈的情况下效率将会非常低。因为在一个线程访问同步方法的时候,其他线程只能阻塞等待。

二、ConcurrentHashMap 的 好处

实现了前面的两个问题 :线程安全了 并且也解决了HashTable 效率低下的问题

三、怎么解决效率低下的问题

这个主要是运用了分段锁(segment)的思想

HashTable 为什么效率低,就是因为它是多个线程竞争同一把锁,那么如果容器里面有很多把锁,这个问题是不是就可以解决了,这个就是ConcurrentHashMap所使用的分段锁技术。

首先将数据分为一段一段的进程存储,然后给每一段分别加上锁,当一个线程占用锁访问其中一个段的数据的时候,其他段的数据也可以被其他线程访问。

接下来我们就重点讲讲分段锁吧

ConcurrentHashMap 是由Segment 数组结构和HashEntry数组结构组成的。Segment是一种可重入锁ReentrantLock,在 ConcurrentHashMap 里面扮演锁的角色,HashEntry 则是用来村塾键值对数据。一个 ConcurrentHashMap 里面包含一个 Segment数组,Segment的结构和HashMap类似,是一种数组+链表结构,一个Segment里面包含一个 HashEntry数组,每个HashEntry是一个链表节点构成的元素,每个Segment守护一个HashEntry数组里面的元素,当对HashEntry数组的元素进行修改时,必须首先获得它对应的Segment锁。可以说,ConcurrentHashMap是一个二级的哈希表。在一个总的哈希表下面还有若干个子哈希表。

在这里插入图片描述

四、采用分段锁技术的好处:并发的读写

case1:不同Segment的并发写入:

在这里插入图片描述

不同的Segment的写入是可以并发执行的

case 2:同一个Segment 的写
在这里插入图片描述

Segment的写入是需要上锁的,因此对同一个Segment的并发写会被阻塞

case 3:同一个Segment 的写-读

同一个Segment的写-读是可以并发执行的

五、详细看看 读-写 的过程
1、读:Get()
  • 为输入的Key做 Hash 运算,得到 hash 值
  • 通过 hash 值,定位到对应的Segment 对象
  • 再次通过 hash 值,定位到 Segment 当中数组的具体位置

读操作其实是没有锁的,第一次通过 hash 定位到 Segment 上,第二次通过 hash 定位到 具体元素上。因为 hashEntry 中的 value 属性是用 volatile 修饰的,保证了可见性,所以每次获取的都死最新值。

2、写:Put()
  • 为输入的 Key 做 Hash 运算,得到 hash 值
  • 通过 hash 值,定位到对应的 Segment 对象
  • 获取可重入锁
  • 再次通过 hash 值,定位到 Segment 当中数组的具体位置
  • 插入或覆盖 HashEntry 对象
  • 释放锁

总结:可以看出ConcurrentHashMap在读写的时候都需要两次定位。首先是定位到 Segment ,然后再定位到 Segment 下的具体的数组下标

六、size() 怎么 解决一致性问题

size()目的是统计ConcurrentHashMap的总元素数量,自然需要把各个Segment 内部的元素都加起来。但是在统计数量的时候,有可能 已经统计过的Segment顺佳插入了新的元素,这个时候应该怎么办?下面我们来看看ConcurrentHashMap的size(),他是一个嵌套循环,大致逻辑如下:

  1. 遍历所有的 Segment
  2. 把 Segment 的元素数量累加起来
  3. 把 Segment 的修改次数累加起来
  4. 判断所有 Segment 的总修改次数是否大于上一次的总修改次数。如果大于,说明统计过程中有修改,重新统计,尝试次数+1;如果不是,说明没有修改,统计结束
  5. 如果尝试次数超过阈值,则对每一个 Segment 加锁,在重新统计
  6. 再次判断所有 Segment 的总修改次数是否大于上一次的总修改次数。由于已经加锁,次数一定和上一次相同
  7. 释放锁,统计结束

总结:一开始对 Segment 不加锁,而是直接尝试将所有的 Segment 元素中的count相加,这样执行两次,然后将两次的结果进行对比,如果两次结果相同则直接返回;如果不相同,则将所有的Segment加锁,然后再次执行统计得到对应的 size 值。

七、再看看1.8是怎么实现的

在这里插入图片描述

1.8 抛弃了分段锁(Segment),而是采取的是 CAS + Synchronized 的方式

将1.7里面的 HashEntry 改为 Node ,但是作用是一样的。

先看看一个概念:乐观锁和悲观锁

悲观锁:认为对于同一个数据的并发操作,一定是为发生修改的

乐观锁:对于同一个数据的并发操作是不会发生修改的,在更新的时候会采取尝试更新不断尝试的方式更新数据

CAS(compare and swap,比较交换)原理:

CAS有三个操作数,内存值V、预期值A、要修改的值B,当且仅当A和V相等时才会将V修改为B,否则什么也不会做。

CAS的缺点:

存在 ABA 问题,解决办法,添加版本号

循环时间长,开销大

只能保证一个共享变量的原子操作

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值