面试系列 | JDK1.7和JDK1.8中ConcurrentHashMap的区别

JDK1.7和JDK1.8中ConcurrentHashMap的区别

 

1、整体结构

JDK1.7:Segment + HashEntry + Unsafe(分段数组+链表)

JDK1.8:移除Segment,使锁的粒度更小,Synchronized + CAS + Node + Unsafe

(数组+链表/红黑二叉树),其实 Node 和 HashEntry 的内容一样,但是HashEntry是一个内部类。

用 Synchronized + CAS 代替 Segment ,这样锁的粒度更小了,并且不是每次都要加锁了,CAS尝试失败了再加锁。

 

2、put()

JDK1.7:先定位Segment,再定位桶,put全程加锁,没有获取锁的线程提前找桶的位置,并最多自旋64次获取锁,超过则挂起。

JDK1.8:由于移除了Segment,类似HashMap,可以直接定位到桶,拿到first节点后进行判断,1、为空则CAS方式插入;2、非null且first.hash==-1则说明在扩容,则跟着一起扩容;3、非null且first.hash != -1则Synchronized锁住 first节点,判断是链表还是红黑树,遍历插入

 

3、get()

基本类似,由于value声明为volatile,java内存模型中的 happen before 规则保证了 对于 volatile 修饰的变量始终是<写操作>先于<读操作> ,并且保证了修改的可见性,因此不需要加锁。

 

4、resize()

JDK1.7:跟HashMap步骤一样(1.new个2倍数组; 2.遍历old数组节点搬去新数组),只不过是搬到单线程中执行,避免了HashMap在JDK1.7中扩容时死循环的问题,保证线程安全。

JDK1.8:支持并发扩容,HashMap扩容在JDK1.8中由头插改为尾插(为了避免死循环问题),ConcurrentHashmap也是,迁移也是从尾部开始,扩容前在桶的头部ForwardingNode放置一个hash值为-1的节点,这样别的线程访问时就能判断是否该桶已经被其他线程处理过了。

 

5、size()

JDK1.7:很经典的思路:计算两次,如果不变则返回计算结果,若不一致,则锁住所有的Segment的count求和。

JDK1.8:由于没有segment的概念,所以只需要用一个 baseCount 变量来记录ConcurrentHashMap 当前节点的个数。先尝试通过CAS 修改 baseCount, 如果多线程竞争激烈,某些线程CAS失败,那就CAS尝试将 CELLSBUSY 置1,成功则可以把 baseCount变化的次数 暂存到一个数组 counterCells 里,后续数组 counterCells 的值会加到 baseCount 中。如果 CELLSBUSY 置1失败又会反复进行CASbaseCount 和 CAScounterCells数组。

 

    有一个很有意思的问题,背景是这样,如何在很短的时间内将大量数据插入到ConcurrentHashMap,换句话说,就是提高ConcurrentHashMap的插入效率,尽量散列均匀和避免加锁两个点等;

    将大批量数据保存到map中有两个地方的消耗将会是比较大的:第一个是扩容操作,第二个是锁资源的争夺。第一个扩容的问题,主要还是要通过配置合理的容量大小和扩容因子,尽可能减少扩容事件的发生;第二个锁资源的争夺,在put方法中会使用synchonized对头节点进行加锁,而锁本身也是分等级的,因此我们的主要思路就是尽可能的避免锁等级。所以,针对第二点,我们可以将数据通过ConcurrentHashMap的spread方法进行预处理,这样我们可以将存在hash冲突的数据放在一个组里面,每个组都使用单线程进行put操作,这样的话可以保证锁仅停留在偏向锁这个级别,不会升级,从而提升效率。

 

 

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值