【Java面试】HashMap最全面试题(三)

简介: 【Java面试】HashMap最全面试题

ConcurrentHashMap 底层具体实现知道吗?实现原理是什么?

JDK1.7

首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。

在JDK1.7中,ConcurrentHashMap采用Segment + HashEntry的方式进行实现,结构如下:

一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和 HashMap类似,是一种数组和链表结构,一个 Segment 包含一个HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个 HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。

Segment的大小默认是16,也就是说 ConcurrentHashMap 有 16 个 Segments,所以理论上,这个时候,最多可以同时支持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上。这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的。再具体到每个 Segment 内部,其实每个 Segment 很像之前介绍的 HashMap,不过它要保证线程安全,所以处理起来要麻烦些。

  1. 该类包含两个静态内部类 HashEntry 和 Segment ;前者用来封装映射表的键值对,后者用来充当锁的角色;
  2. Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里得元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁。

JDK1.8

在JDK1.8中,放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N 倍!!!。

看插入元素过程(建议去看看源码):

如果相应位置的Node还没有初始化,则调用CAS插入相应的数据;

1 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
2   if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
3   break; // no lock when adding to empty bin
4 }

如果相应位置的Node不为空,且当前该节点不处于移动状态,则对该节点加synchronized锁,如果该节点的hash不小于0,则遍历链表更新节点或插入新节点;

1 if (fh >= 0) {
2 binCount = 1;
3 for (Node<K,V> e = f;; ++binCount) {
4 K ek;
5 if (e.hash == hash &&
6 ((ek = e.key) == key ||
7 (ek != null && key.equals(ek)))) {
8 oldVal = e.val;
9 if (!onlyIfAbsent)
10 e.val = value;
11 break;
12 }
13 Node<K,V> pred = e;
14 if ((e = e.next) == null) {
15 pred.next = new Node<K,V>(hash, key, value, null);
16 break;
  1. 如果该节点是TreeBin类型的节点,说明是红黑树结构,则通过 putTreeVal方法往红黑树中插入节
    点;如果binCount不为0,说明put操作对数据产生了影响,如果当前链表的个数达到8个,则通过
    treeifyBin方法转化为红黑树,如果oldVal不为空,说明是一次更新(覆盖)操作,没有对元素个数产生影响,则直接返回旧值;
  2. 如果插入的是一个新节点,则执行addCount()方法尝试更新元素个数 baseCount;

HashTable,HashMap,TreeMap区别?

  1. HashTable线程同步,HashMap,TreeMap非线程同步。
  2. HashTable,TreeMap不允许<键,值>有空值,HashMap允许<键,值>有空值。
  3. HashTable中hash数组的默认大小是11,增加方式的old*2+1,HashMap中hash数组的默认大小
    是16,增长方式一定是2的指数倍。
  4. TreeMap能够把它保存的记录根据键排序,默认是按升序排序,因此如果你的键需要有序,建议使用TreeMap代替HashMap。

HashMap的扩容因子(HashMap 什么时候进行扩容呢)重点!

扩容因子:尺寸/容量。空表的负载因子是0,半满表的负载因子是0.5,以此类推。负载轻的表产生冲突的可能性小,因此对于插入和查找都是最理想的状态(但是会减慢使用迭代器遍历的过程)。HashMap 与HashSet 都允许指定负载因子的构造器,表示当负载情况达到该负载因子的水平时,容器将会自动扩容(增加桶位数),实现方式时使容量大致加倍,并重新将现有对象分布到新的桶位集中(这个过程被称为再散列)。

当HashMap中的元素越来越多的时候,碰撞的几率也就越来越高(因为数组的长度是固定的),所以为了提高查询的效率,就要对HashMap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,所以这是一个通用的操作,很多人对它的性能表示过怀疑,不过想想我们的“均摊”原理,就释然了,而在hashmap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。

  那么HashMap什么时候进行扩容呢?当hashmap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过16 * 0.75=12的时候,就把数组的大小扩展为2 *16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,不过上面已经说过,即使是1000,hashmap也自动会将其设置为1024。 但是new HashMap(1024)还不是更合适的,因为0.75 * 1024 < 1000, 那么再次插入数据的时候就可能发生数组扩容了,也就是说为了让0.75 * size > 1000,我们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。

加载因子(扩容因子)为何默认是0.75?

  • 在空间占用与查询时间之间取得了较好的权衡
  • 大于这个值,空间节省了,但是链表可能过长就会影响性能
  • 小于这个值,冲突减少了,但是扩容就会比较频繁,空间占用多并且扩容也会消耗性能

多线程修改HashMap

多线程同时写入,同时执行扩容操作,多线程扩容可能死锁、丢数据;可以对HashMap 加入同步锁Collections.synchronizedMap(hashMap),但是效率很低,因为该锁是互斥锁,同一时刻只能有一个线程执行读写操作,这时候应该使用ConcurrentHashMap

注意:在使用Iterator遍历的时候,LinkedHashMap会产生java.util.ConcurrentModificationException

扩展HashMap增加双向链表的实现,号称是最占内存的数据结构。支持iterator()时按Entry的插入 顺序来排序(但是更新不算,如果设置accessOrder属性为true,则所有读写访问都算)。实现上是

在Entry上再增加属性before/after指针,插入时把自己加到Header Entry的前面去。如果所有读

写访问都要排序,还要把前后Entry的before/after拼接起来以在链表中删除掉自己。

多线程下使用HashMap会有什么问题?

扩容死链(JDK1.7)

HashMap为什么会产生死循环?

数据错乱(JDK1.7/8)

多线程情况下,可能出现两个线程同时插入的数据要插入到同一个索引处,也就是key不同,但是hashcode相同的情况,然后可能出现两个线程都进入到了为这个为同一个索引添加新节点的情况,那么先来的数据先赋值完毕后,另一个数据再赋值,那么就会出现覆盖问题。

这里我使用IDEA的debug并且设定进入断点的条件,来模拟这种情况的发生

下面是线程1,线程1插入的数据的索引为1

下面是线程2,线程2要插入的数据的索引也为1

我让线程2先完成数据的插入,那么此时线程1中的情况如下

之后我让线程1执行完毕插入操作,那么此时的情况如下,出现了数据的覆盖

可以发现一样的代码,但是只有一个key的数据了,这就是数据错乱

为何要用红黑树?

链表在链表长度过长的时候,遍历的时间复杂度为O(n),而红黑树的时间复杂度为O(logn),因此在链表长度过长的时候使用红黑树可以加快遍历速度。

为何不一上来就树化?

这是基于时间和空间的综合考虑。

时间上,在Hash冲突比较小的时候,即使转换为了红黑树,也不会有效率上多大的提升,反倒会导致在put的时候由于需要进行红黑树的旋转算法,所以反倒会导致时间效率降低。

红黑树的节点类型为TreeNode,而链表的节点类型为Node,红黑树消耗的空间更多,需要维护更多的指针,对内存的占用更大。

而HashMap之所以选择红黑树而不是普通的二叉搜索树,是因为普通的二叉搜索树在特殊的情况它会变成一个倾斜的情况,最差的情况会变成一个类似于链表的情况,那么查找效率就会退化为和链表差不多。而红黑树他是可以防止这种退化的,并且它又不像其他的完全的平衡二叉树那样有严格的平衡条件,因此红黑树它的插入效率又比那种完全的平衡二叉树要高,因此选择红黑树可以防止极端环境下的退化,又可以兼顾查询和插入的效率。

树化阈值为何是8?

其实,一个链表的长度能超过8的概率非常低。

红黑树用来避免DoS攻击,防止链表超长时性能下降,树化应当是偶然情况,

hash表的查找,更新的时间复杂度是0(1),而红黑树的查找,更新的时间复杂度是0(log2n),TreeNode占用空间也比普通Node的大,如非必要,尽量还是使用链表。

hash值如果足够随机,则在 hash表内按泊松分布,在负载因子0.75的情况下,长度超过8的链表出现概率是0.00000006,选择8就是为了让树化几率足够小.

何时会树化?何时会退化为链表?

树化有两个条件:链表长度超过树化阈值并且数组容量大于等于64。

由于树化的开销很大,因此能使用扩容的方式解决链表长度过长的话,是不会树化的。

退化情况1:在扩容的时候如果拆分了树,使得树中元素个数小于等于6,那么红黑树会退化为链表。

退化情况2:remove树中的节点的时候,在remove之前,若root,root.left,root.right,root.left.left中有一个为null,也会退化为链表,然后再移除这个节点。

HashMap为什么会产生死循环?

其他集合类型面试题

Set集合

List集合(重要)

相关文章
|
4月前
|
缓存 Java 关系型数据库
2025 年最新华为 Java 面试题及答案,全方位打造面试宝典
Java面试高频考点与实践指南(150字摘要) 本文系统梳理了Java面试核心考点,包括Java基础(数据类型、面向对象特性、常用类使用)、并发编程(线程机制、锁原理、并发容器)、JVM(内存模型、GC算法、类加载机制)、Spring框架(IoC/AOP、Bean生命周期、事务管理)、数据库(MySQL引擎、事务隔离、索引优化)及分布式(CAP理论、ID生成、Redis缓存)。同时提供华为级实战代码,涵盖Spring Cloud Alibaba微服务、Sentinel限流、Seata分布式事务,以及完整的D
203 2
|
3月前
|
缓存 Java API
Java 面试实操指南与最新技术结合的实战攻略
本指南涵盖Java 17+新特性、Spring Boot 3微服务、响应式编程、容器化部署与数据缓存实操,结合代码案例解析高频面试技术点,助你掌握最新Java技术栈,提升实战能力,轻松应对Java中高级岗位面试。
337 0
|
4月前
|
存储 安全 Java
2025 最新史上最全 Java 面试题独家整理带详细答案及解析
本文从Java基础、面向对象、多线程与并发等方面详细解析常见面试题及答案,并结合实际应用帮助理解。内容涵盖基本数据类型、自动装箱拆箱、String类区别,面向对象三大特性(封装、继承、多态),线程创建与安全问题解决方法,以及集合框架如ArrayList与LinkedList的对比和HashMap工作原理。适合准备面试或深入学习Java的开发者参考。附代码获取链接:[点此下载](https://pan.quark.cn/s/14fcf913bae6)。
1467 48
|
4月前
|
算法 架构师 Java
Java 开发岗及 java 架构师百度校招历年经典面试题汇总
以下是百度校招Java岗位面试题精选摘要(150字): Java开发岗重点关注集合类、并发和系统设计。HashMap线程安全可通过Collections.synchronizedMap()或ConcurrentHashMap实现,后者采用分段锁提升并发性能。负载均衡算法包括轮询、加权轮询和最少连接数,一致性哈希可均匀分布请求。Redis持久化有RDB(快照恢复快)和AOF(日志更安全)两种方式。架构师岗涉及JMM内存模型、happens-before原则和无锁数据结构(基于CAS)。
117 5
|
4月前
|
Java API 微服务
2025 年 Java 校招面试全攻略:从面试心得看 Java 岗位求职技巧
《2025年Java校招最新技术要点与实操指南》 本文梳理了2025年Java校招的核心技术栈,并提供了可直接运行的代码实例。重点技术包括: Java 17+新特性(Record类、Sealed类等) Spring Boot 3+WebFlux响应式编程 微服务架构与Spring Cloud组件 Docker容器化部署 Redis缓存集成 OpenAI API调用 通过实际代码演示了如何应用这些技术,如Java 17的Record类简化POJO、WebFlux构建响应式API、Docker容器化部署。
156 5
|
4月前
|
缓存 NoSQL Java
Java Redis 面试题集锦 常见高频面试题目及解析
本文总结了Redis在Java中的核心面试题,包括数据类型操作、单线程高性能原理、键过期策略及分布式锁实现等关键内容。通过Jedis代码示例展示了String、List等数据类型的操作方法,讲解了惰性删除和定期删除相结合的过期策略,并提供了Spring Boot配置Redis过期时间的方案。文章还探讨了缓存穿透、雪崩等问题解决方案,以及基于Redis的分布式锁实现,帮助开发者全面掌握Redis在Java应用中的实践要点。
203 6
|
4月前
|
安全 Java API
2025 年 Java 校招面试常见问题及详细答案汇总
本资料涵盖Java校招常见面试题,包括Java基础、并发编程、JVM、Spring框架、分布式与微服务等核心知识点,并提供详细解析与实操代码,助力2025校招备战。
201 1
|
4月前
|
算法 Java 微服务
2025 年 Java 面试宝典社招春招秋招实操全方位攻略
2025年Java面试宝典涵盖核心技术及最新趋势,分为四大板块:1. Java基础:深入数据类型、多态等特性,结合学生信息管理等实例;2. JVM核心:解析内存模型与GC算法,附多线程转账等场景应用;3. 高并发方案:详解synchronized与线程池配置,提供Web服务器优化案例;4. Spring生态:剖析IoC/AOP原理,演示微服务架构实现。特别新增Java 17+特性实操,包括Record类、密封接口等语法糖,整合Spring Boot 3、响应式编程及云原生技术,通过订单状态机、API网关配置。
270 1
|
4月前
|
NoSQL Java 微服务
2025 年最新 Java 面试从基础到微服务实战指南全解析
《Java面试实战指南:高并发与微服务架构解析》 本文针对Java开发者提供2025版面试技术要点,涵盖高并发电商系统设计、微服务架构实现及性能优化方案。核心内容包括:1)基于Spring Cloud和云原生技术的系统架构设计;2)JWT认证、Seata分布式事务等核心模块代码实现;3)数据库查询优化与高并发处理方案,响应时间从500ms优化至80ms;4)微服务调用可靠性保障方案。文章通过实战案例展现Java最新技术栈(Java 17/Spring Boot 3.2)的应用.
231 9
|
4月前
|
缓存 算法 NoSQL
校招 Java 面试高频常见知识点深度解析与实战案例详细分享
《2025校招Java面试核心指南》总结了Java技术栈的最新考点,涵盖基础语法、并发编程和云原生技术三大维度: 现代Java特性:重点解析Java 17密封类、Record类型及响应式Stream API,通过电商案例演示函数式数据处理 并发革命:对比传统线程池与Java 21虚拟线程,详解Reactor模式在秒杀系统中的应用及背压机制 云原生实践:提供Spring Boot容器化部署方案,分析Spring WebFlux响应式编程和Redis Cluster缓存策略。
94 1

热门文章

最新文章