彻底掌握HashMap集合相关面试题

本文详细介绍了HashMap与ConcurrentHashMap的区别,包括线程安全问题的原因及解决方案。HashMap在并发环境下可能出现数据覆盖和扩容死锁,解决方法包括使用HashTable、synchronizedMap或ConcurrentHashMap。Java8的ConcurrentHashMap放弃了分段锁,采用CAS和synchronized保证线程安全。此外,文章还讨论了HashMap的哈希冲突解决策略和Java1.8的优化点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

收藏吧 还等啥??

一:HashMap与ConcurrentHashMap的区别?

首先两者的相同点在于底层都是数组+链表实现实现的。
但两者最大的区别就是HashMap是线程非安全的,ConcurrentHashMap是线程安全的。

二:HashMap为什么会有线程安全问题?

我们知道jdk1.7和jdk1.8中HashMap都是线程不安全的,那我们就具体讲一下为什么会线程不安全(两个方面:调用put方法、扩容)。

①调用put方法

假如有两个线程A和B,A希望插入一个key-value到HashMap中,首先会通过A的key得到桶的索引坐标,然后获取该桶的链表头结点,线程A的时间片用完,而此时B线程被调用执行,和线程A一样执行,只不过线程B成功的将数据插入到桶里面。假设线程A插入时候计算的坐标和B线程要插入的索引坐标是一致的,那么当B线程成功插入以后,线程A再次被调用运行的时候,它依然持有原来的链表头,但是它对B线程插入的过程一无所知,那么线程A就会对此坐标上的数据进行覆盖,那么线程B插入的数据就会消失,造成数据不一致的行为。

②扩容

JDK7版本中的HashMap扩容时使用头插法,假设此时有元素一指向元素二的链表,当有两个线程使用HashMap扩容的时,若线程一在迁移元素时阻塞,但是已经将指针指向了对应的元素,线程二正常扩容,因为使用的是头插法,迁移元素后将元素二指向元素一。此时若线程一被唤醒,在现有基础上再次使用头插法,将元素一指向元素二,形成循环链表。若查询到此循环链表时,便形成了死锁。而JDK8版本中的HashMap在扩容时保证元素的顺序不发生改变,就不再形成死锁,但是注意此时HashMap还是线程不安全的。

三:如何解决HashMap线程安全问题?

解决线程安全问题的方式有多种比如:

①比如使用HashTable

②使用Collection.synchronizedMap

③使用ConcurrentHashMap

在使用HashTable或者Collection.synchronizedMap.但是这两者有着共同的问题:那就是性能问题。无论读操作还是写操作,它们都会给整个集合进行加锁,导致同一时间内其他的操作进入阻塞状态。
那么在并发环境下,能够兼顾线程安全和运行效率的情况下就要使用concurrentHashMap.concurrentHashMap在JDK1.7和1.8的版本改动比较大,1.7使用Segment+HashEntry分段锁的方式实现,而1.8则抛弃了Segment,改为使用CAS+synchronized+Node实现,同样也加入了红黑树,避免链表过长导致性能的问题。

四:Java8的ConcurrentHashMap为什么放弃了分段锁?存在什么弊端?

从结构上说,1.7版本的ConcurrentHashMap采用分段锁机制,里面包含一个Segment数组,实际上就是相当于每个Segment都是一个HashMap,默认的Segment长度是16,也就是支持16个线程的并发写,Segment之间相互不会受到影响。我们看一下ConcurrentHashMap的几种并发情况。

①:不同Segment的并发写入互相不影响

1.7ConcurrentHashMap并发写入数据
②同一Segment的写和读是可以并发执行的
1.7ConcurrentHashMap并发写查数据
③同一Segment的并发写入(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)

1.7ConcurrentHashMap并发对同一个段进行插入数据
分段锁的优势在于保证在操作不同段 map 的时候可以并发执行,操作同段 map 的时候,进行锁的竞争和等待。这相对于直接对整个map同步synchronized是有优势的。

弊端在于分成很多段时会比较浪费内存空间(不连续,碎片化); 操作map时竞争同一个分段锁的概率非常小时,分段锁反而会造成更新等操作的长时间等待; 当某个段很大时,分段锁的性能会下降。

Java1.8之后的ConcurrentHashMap取消了分段锁,而采用CAS和synchronized来保证并发安全问题。
ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术
有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。

五:HashMap原理?哈希冲突产生的原因?如何解决冲突?

①:HashMap原理图
jdk8-hashMap存储原理

②:处理哈希冲突的方法

开放定址法、链地址法、再散列法

在hashMap中是采用链地址法的方式解决哈希冲突的。

首先说一下什么是哈希冲突:
我们在调用put方法的时候,会根据key的hashCode值通过hash函数计算得到插入的位置,如果不同的key插入到同一个位置,这种情况叫hash冲突,hashMap采用链表去解决冲突。

③:如何解决哈希冲突

HashMap的单个bucket里存储的不是一个 Entry,而是一个 Entry 链(数组+链表结构)。当我们在进行put的时候,根据key的hashCode通过hash函数计算出bucket桶中的index,然后通过key的equals方法去判断链表中是否存在相同key,如果存在则直接替换value,不相等则新建node加入链表中。

六 hash函数java1.8相比Java1.7做的主要优化:

①:数组+链表改成了数组+链表或红黑树;(链表转红黑树是链表长度阈值为8,红黑树转链表阈值为6)

②:链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,1.7将新元素放到数组中,原始节点作为新节点的后继节点,1.8遍历链表,将元素放置到链表的最后;

③:扩容的时候1.7需要对原数组中的元素进行重新hash定位在新数组的位置,1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小;
在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容;

结束

识别下方二维码!回复: 入群 ,扫码加入我们交流群!

点赞是认可,在看是支持

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值