ITPub博客

首页 > 数据库 > 数据库开发技术 > HashMap和ConcurrentHashMap的灵魂拷问

HashMap和ConcurrentHashMap的灵魂拷问

原创 数据库开发技术 作者:IT枫斗者 时间:2022-02-26 22:21:42 0 删除 编辑


简介:大家好,我是枫哥,🌟一线互联网的IT民工、📝资深面试官、🌹Java跳蚤网课堂创始人。拥有多年一线研发经验,曾就职过科大讯飞、美团网、平安等公司。在上海有自己小伙伴组建的副业团队,目前业余时间专注Java技术分享,春招/秋招/社招/跳槽,一对一学习辅助,项目接活开发。


🌟 欢迎关注 🌟 收藏 🌹在看 🌹


🍊🍊:文末送福利





前言




Map 这样的键值对key-value结构在软件开发中是非常经典的结构,常用于在内存中存放数据。


看了些技术文章,总结了一下HashMap和ConcurrentHashMap的核心知识点,采用提问解答的方式,相信看完后以后你会感觉清晰很多~

 


一、谈谈你理解的 HashMap,讲讲其中的 get put 过程。




答:HashMap 底层是基于数组+链表组成的容器,key-value允许空值,不同步、线程不安全


Put过程:


  1. 先判断数组是否需要初始化;

  2. 如果key为空,则put一个null值进去;

  3. 根据key计算出hashcode;

  4. 根据hashcode定位出所在的捅;

  5. 如果捅是一个链表,则要判断里面的hashcode、key是否和传入的key相等,如果相等则进行覆盖,并返回原来的值;

  6. 如果捅是空的,说明当前位置没有数据存入,新增一个Entry对象写入当前位置;

  7. 当调用 addEntry 写入 Entry 时需要判断是否需要扩容。


Get过程:


  1. 根据key计算hashcode,定位到具体的桶;

  2. 判断该位置是否是链表,不是链表就根据key、key的hashcode是否相等来返回值,是链表就遍历直到key以及hashcode相等的时候就返回值;

  3. 啥都没取到就返回null.



二、1.8 做了什么优化?



  1. 优化了查询效率,TREEIFY_THRESHOLD 用于判断是否需要将链表转换为红黑树的阈值。

  2. HashEntry 修改为 Node。

  3. 对大链表做了优化,修改为红黑树之后查询效率直接提高到了 O(logn)。



三、HashMap是线程安全的嘛?



不是,jdk没有对HashMap做任何同步操作


  • 不安全会导致哪些问题?


并发环境下容易产生死循环,发生死锁


  • 如何解决?有没有线程安全的并发容器?


可以使用Collections.synchronizedMap(Map map)方法,可以把hashmap变成一个同步的容器 (拥有锁限制的同步机制,内部加了synchronized锁同步关键字)有,ConcurrentHashMap,内部采用了ReentrantLock可重入锁,有效避免死锁



四、ConcurrentHashMap 是如何实现的?



由segment数组,HashEntry组成,仍是数组+链表,与HashMap类似,只是内部的Value以及链表都是volatile关键字修饰,保证其可见性。


ConcurrentHashMap使用了分段锁技术,segment继承于RenntrantLock可重入锁,默认16个,线程相互独立。(1.7)


1.7、1.8 实现有何不同?为什么这么做?


1.7:



Put操作:(由于volatile只能保证数据的可见性和顺序性,无法保证原子性,所以仍需加锁)


首先通过key定位到segment,然后再到具体的segment进行put,


将当前segment中的table根据key的hashcode定位到HashEntry,遍历该HashEntry,不为空判断传入的key和当前遍历的key是否相等,相等则覆盖旧的值。为空则新建一个HashEntry加入当前segment中,并判断是否需要扩容。最后解除获取的segment锁。


Get操作: 


只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。

由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。

ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁


1.8:(解决那就是查询遍历链表效率太低。)




1.8 抛弃了segment分段锁,取消了 ReentrantLock 改为了synchronized,采用CAS+synchronized保证并发安全性。


将Entry改为Node,val和next用volatile修饰。


采用红黑树之后可以保证查询效率


CAS:CompareAndSwag比较交换,实现多线程同步的原子指令


Put操作:


  1. 首先根据key计算hashcode;

  2. 判断是否需要初始化;

  3. 通过key定位Node,判断是否为空。空:可直接写数据,CAS尝试写入,若失败,则自旋写入;

  4. 若当前Node位置 hashcode==MOVED==-1 ,则可扩容;

  5. 若都不满足则synchronized锁简单粗暴写;

  6. 若数量大于treeify_thresod树阈值。则转为红黑树


Get操作:


  1. 根据key hashcode 寻址。

  2. 若在桶上,直接返回,若是红黑树,就按照树的方式遍历。

  3. 若都不满足就用链表的方式遍历。

 


五、HashMap什么时候扩容?



什么时候扩容:


  • 当向容器添加元素(存值)的时候,会判断当前容器的元素个数,如果大于等于阈值---即当前数组的长度乘以加载因子的值的时候,就要自动扩容

  • 而在hashmap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。 

  • 所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。





原文作者:IT枫斗者;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论
大家好,我是枫哥,🌟阿里云技术专家、📝资深面试官、🌹Java跳蚤网课堂创始人。技术交流vx:itfdz666,拥有多年一线研发经验,曾就职过科大讯飞、美团网、平安等公司。看面试实战视频可以关注b站号:IT枫斗者–Java面试。需要看技术文章,可以关注CSDN:IT枫斗者。

注册时间:2020-05-13

  • 博文量
    183
  • 访问量
    81379