栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

HashMap灵魂26问

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

HashMap灵魂26问

1.面试问聊聊HashMap、源码,实现原理
  • Java 1.7 HashMap 是由 数组+链表 数据结构。
    • 数组的长度是固定的,因此会存在哈希碰撞,也就是下标位置冲突,
    • 那么就会形成链表。若链表长度过长就会造成查询效率过慢,时间复杂度为O(n)。
  • Java 1.8 数据结构就变成了 数组+链表+红黑树。
    • 每个数据单元都是一个Node结构,结构中有Key ,value next ,hash 这几个属性。
    • next 字段是在Hash冲突后形成链表需要的。
    • 当链表长度超过阈值8,则调用转红黑树的方法,此时还判断若数组长度小于64的情况下,只会扩容, >=64转换为红黑树。主要为了提高查询效率。
  • Java 1.7 采用的是 头插法,就是说新插入的值的时候,原来的值就会往后推一位,让新值放在头部位置,这样做的原因是因为后插入的值查找的可能性会更大一点,提高查找的效率。但是这样就会造成一个问题,在同一个位置上新元素总会被放在链表的头部位置,如果多线程put的情况下,就会改变数据的引用结构,那么就会造成环形链表,发生死循环!
  • Java 1.8 之后采用的是 尾插法,这样扩容转移后前后链表顺序不变,保持之前节点的引用关系,就不会出现死循环的情况。
2. Put过程

注:Java1.8HashMap

  • 首先对 Key求Hash值,通过 key 的 hashCode 高低16位异 + 按位与运算 & ( table.length-1 ),计算所在数组下标。
    • Hahs计算: key 为 null 返回 0,否则就将 key 的 hashCode 高低16位异或为了降低哈希冲突概率。
  • 判断数组table是否为空。为空则进行初始化(这里是一个懒加载机制)。

以下分好几种情况

  • 第一种 ,该位置为null,
    • 将传入的内容封装成Node对象占用这个位置
  • 第二种 该位置不为空,并且 node 还未链表
    • 对比Node的key 与当前put的 key是否相同,若相同则执行更新操作。
    • 若Key不同,则表示发生Hash冲突了 。会在Node节点后边追加一个新的Node,使用尾插法。
  • 第三种情况, node 节点已经链表化
    • 遍历链表
    • 对比一下Node的key 与当前put的 key是否相同,若相同则执行更新操作,这里直接break跳出循环了。
    • 若遍历到尾节点,也未匹配到Key不同,会在链表后边追加一个新的Node。
    • 判断当链表的长度达到树化的阈值8,若超过则调用转红黑树的方法
    • 转红黑树方法中判断,若数组长度<64的情况下,只会扩容 。数组>=64 并且链表长度超过8,才转红黑树
  • 第四种情况, Node 节点已经转换成红黑树,TreeNode 类型
    • 调用 putTreeval 方法增加树节点,
    • 比较插入节点和当前节点的大小,若节点hash值小就往左子树查找,否则往右子树查找,
    • 找到空位后执行调整平衡 balanceInsert 方法,插入节点并调整平衡;
    • 执行重置根节点 moveRootToFront 方法,由于调整平衡后根节点可能变化,所以需要重置根节点 ;
  • 添加完元素,后将 modCount 加 1
  • 最后判断时候需要扩容,若超过百分之75的占用则进行扩容。

ps: 使用class对象作为Key需要重写该对象的hashCode()方法与equals()方法。

3.Get方法执行过程
  • 根据key 求Hash 并且与运算求出数组位置
  • 若数组位置是非树,非链表则返回当前值,
  • 若是链表或者红黑树则遍历,直到取到key与查找的key相同的数据,或查到没有数据。
4.初始化大小与创建时机
  • 默认初始化容量是 16
  • 懒加载机制,第一次Put数据的时候创建。
5.扩容过程
  • 默认的负载因子大小为0.75,限制最大不超过Integer的最大值。
  • 当一个map占用达到容量75%的时候
  • 将会创建原来HashMap大小的两倍的新Map,
  • 遍历旧Map按照每个桶位迁移
    • 第一种情况:当前位置,存储的是null ( 无需处理 )
    • 第二种情况:当前位置,存储的是Node 没有下级Next
      • 没有发生过hash冲突,使用[ hash值 & (新容量-1) ] 计算下标后放入新Map
    • 第三种情况:当前桶位存储的是链化的Node
      • 遍历链表,拆分成两个链表,高位链,与低位链
      • 通过hash值 & (与运算)旧容量 只可能是 0或1 ,一个是原下标的位置,另一种是在下标为<原下标+原容量>的位置
    • 第四种情况:当前桶位存储的是红黑树根节点TreeNode对象
      • 红黑树结构保留了Next字段,其实红黑树内部还维护者一个链表。虽然查询不用但是增删改的时候还维护了这个字段
      • 主要用于方便 split 拆分 这个红黑树,根据高低位分成高位链,与低位链。低位链是原位置, 高位链存放在新表的下标老表的位置+老表的容量。
      • 不管是高位链还是低位链表,拆分的链表,需要判断一下长度
      • 若长度<=6 直接将treeNode 转换成普通的链表,放到扩容后的新表中
      • 若扔> 6 还会升级成红黑树。
6.扩容后数据在新表的位置?以及原因

旧位置或 原下标+原容量

原先链表已经链化,推理出所有Node的hash字段转化成二进制后低位都是相同的,低位值就是老表的 size -1 ,转化出来的二进制数的有效位
比如 table 16 ;16-1=15 ,15转化成二进制1111 。 说明低位是低四位,高位是第5位 (ps:0 1111)
当链表低位是相同的,但是高位可能不一样,有的node 可能是0 ,有的 node 高位可能是 1
对应迁移到新表

低位链,因为高位是0 ,所以迁移到node 新表的时候,这个 slot下标和老的一样。
高位链,因为高位是1 , 所以存储到新表之后。是老表的位置+老表的容量。

7.为什么都是2的N次幂的大小。
  • HashMap通过哈希算法得出哈希值之后,决定将键值放入对应的索引位 ;

  • HashMap的容量为16转化成二进制为10000,length-1得出的二进制为01111;

    总结:因为2的幂-1,转化成二进制都是11111结尾的,所以碰撞几率小。

8.什么时候转红黑树
  • 链表长度计数达到 8(put第9个的时候)
  • 并且数组长大于64转换为红黑树。
    • 若数组长度<64的情况下,只会扩容
9.红黑树TreeNode 对象的基本结构?
  • TreeNode 继承与 linkedHashMap.Entry 而linkedHashMap.Entry 继承与HashMap.Node
  • 父节点(parent) 、 左指向(left)、右指向(right)、前一个节点(prev)、颜色标记(red)
10.红黑树写入操作流程?
  • 首先找到一个合适的插入点,找到插入节点的父节点
  • 红黑树由于满足二叉树的所有特性(左边的小,右边的大,每次向下查找一层就可以排除掉一半数据 todo 待看源码验证)
  • 第一种情况;一直向下查找,直到查到左子树或右子树为null,说明整个树,没发现 node key 与当前put key一致的TreeNode
    • 此时将当前插入的节点,就是父节点的左子树或者是右子树
    • 根据插入节点的hash值和父节点的hash值大小决定左右。
    • 插入会打破平衡,还需调用红黑树的平衡算法
  • 第二种情况 :根节点向下探测过程中,若发现treeNode的key与put的key完全一致,说明是更新操作。直接更新就好。
    • 替换完成后还需要看一下是否满足二叉查找树的特性.
11.JDK8中对HashMap做了怎样的优化:
  • 数组+链表改成了数组+链表或红黑树;
    • JDK1.8中,HashMap采用位桶+链表+红黑树实现,当链表长度超过阈值(8)并且数组长度大于64,将链表转换为红黑树,大大减少了查找时间。
  • 链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,1.7将新元素放到数组中,原始节点作为新节点的后继节点,1.8遍历链表,将元素放置到链表的最后;
  • 扩容的时候1.7需要对原数组中的元素进行重新hash定位在新数组的位置,1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小;
  • 在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容;
12.头插法和尾插法

头插法 (java 1.7 使用此法):

  • 不用遍历链表,速度快;
  • 但是再扩容操作时,多线程操作下会出现 Entry 的 next 指针 并发修改导致数据丢失或死循环

尾插法 (java 1.8 使用此法)::

  • 需要遍历全链表。
  • 不会出现闭环引用问,但是在多线程下也可能数据丢失。
13.为什么要转红黑树,为什么是阈值>=8。

为什么使用红黑树:

  • 为了解决Hash 链化问题
  • 链化问题会影响get 效率,本身散列查询复杂度 O(1)
  • 链化后会导致退化为O(n)
  • 红黑树其实就是一颗特殊的二叉排序树。

为什么一开始不直接用红黑树?

  • 红黑树的插入慢(鱼和熊掌不可兼得)

为什么阈值>=8 ?:(其实这里不严谨,阈值>=8 && 数组长度 >= 64 才进行转红黑树,若数组< 64 则会进行扩容 )

  • 在随机哈希代码下,桶中的节点频率遵循泊松分布,桶的长度超过8的概率非常非常小。
  • 所以作者应该是根据概率统计而选择了8作为阀值。
14.红黑树转链表,为什么是阈值<=6。
  • 中间有个差值7可以防止链表和树之间频繁的转换

假设一下:如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低

15.HashMap 为什么线程不安全?

JDK7 存在死循环和数据丢失问题。
①数据丢失:

  • 并发赋值被覆盖: 在 createEntry新增节点 方法中,新节点头插法,若两个线程同时执行到此处,可导致其中一个线程的赋值被覆盖。
  • 已遍历区间新增元素丢失: 当某个线程在 transfer 方法迁移时,其他线程新增的元素可能落在已遍历过的哈希槽上。遍历完成后,table 数组引用指向了 newTable,新增元素丢失。
  • 新表被覆盖: 如果 resize 完成,执行了 table = newTable,则后续元素就可以在新表上进行插入。但如果多线程同时 resize ,每个线程都会 new 一个数组,这是线程内的局部对象,线程之间不可见。迁移完成后resize 的线程会赋值给 table 线程共享变量,可能会覆盖其他线程的操作,在新表中插入的对象都会被丢弃。

② 死循环:
扩容时 resize 调用 transfer 使用头插法迁移元素,虽然 newTable 是局部变量,但原先 table 中的 Entry 链表是共享的,
问题根源是 Entry 的 next 指针 并发修改,某线程还没有将 table 设为 newTable 时用完了 CPU 时间片,导致数据丢失或死循环。

JDK8 在 resize 方法中完成扩容,并改用尾插法,不会产生死循环,但并发下仍可能丢失数据

16.HashMap存在线程安全问题,那是怎么处理这种情况的?
  • ConcurrentHashMap
  • HashTable
  • 使用 Collections.synchronizedMap(new HashMap<>())
17.在jdk1.7高并发下如果没有处理线程安全会有怎样的安全隐患,具体表现是什么。
  • 在java1.7 头插时,PUT操作的时候,假如A线程和B线程同时对同一个数组位置,调用添加节点方法

在hashmap做put操作的时候, 假如A线程和B线程同时对同一个数组位置调用addEntry,两个线程会同时得到现在的头结点,然后A写入新的头结点之后,B也写入新的头结点,那B的写入操作就会覆盖A的写入操作造成A的写入操作丢失

18.何为快速失败(fail-fast)
  • fail-fast 是一种错误检测机制,并发情况下多个线程对集合的结构进行修改是就会触发 fail-fast机制,抛出ConcurrentModificationException异常。
  • 原理: 迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量,遍历期间集合如果发生变化就会改变modCount的值,等下一个hasNext()/next()的时候就会比对modCount != expectedModCount 这个判断条件,如果为true则会抛出异常,终止遍历,否则就返回遍历。
19.红黑树左旋或右旋
  • 左旋:右节点为当前节点,代替父节点,左孩子分支给父节点作为右孩子分支
20.Node节点Hash字段的值,是key对象HashCode返回值?
  • 不是
  • 是 key 的 hashCode 高低16位异或
    • 为了了降低哈希冲突概率。
21.HashMap,HashTable,ConcurrentHashMap的区别。
HashMapHashtableConcurrentHashMap
继承的父类AbstractMapDictiionaryAbstractMap
线程安全性不安全安全安全
synchronized锁方法jdk 1.6 采用分段的数组+链表实现 jdk 1.8 采用一种乐观锁,Cas机制实现
是否同步
k,v可否null否(空指针异常)
初始容量161116
扩容方式2倍(2的整数次幂)2倍+12倍(2的整数次幂)
哈希值的使用不同HashMap重新计算hash值直接使用对象的hashCode
22.HashMap和ConcurrentHashMap的区别
  • 都是无序的,底层数据结构都是 数组+链表+红黑树
  • HashMap 线程不安全,ConcurrentHashMap线程安全
    • jdk1.8的实现:采用CAS + Synchronized,synchronized在put的时候锁是节点的第一个元素,但其底层还是“数组+链表+红黑树”的实现。当链表长度大于8并且数组长度大于64的时候转红黑树。
  • k,v可否null HashMap 可以为 null , ConcurrentHashMap 不可以
  • 初始容量 ,都是16
  • 扩容方式 , 2倍
23.HashMap和TreeMap区别

HashMap

  • 线程非安全
  • 数组加链表方式存储key/value, java1.8之后新加了红黑树
  • 允许null作为key和value
  • key不可以重复,value允许重复
  • 不保证元素迭代顺序是按照插入时的顺序;

TreeMap

  • 基于红黑树
  • 线程非安全
  • 不允许null作为key
  • key不可以重复,value允许重复
  • 默认按照key的字典顺序来排序(升序),自定义排序规则需要实现Comparable接口
24.Hash的基本概念
  • 将任意长度的输入通过hash算法映射成固定长度
25.Hash冲突可以避免?
  • 理论上无法避免。可以减少碰撞机率。
26.Hash算法应该考虑哪些点?
  • 效率高
  • 长文本也可以高效计算
  • 通过hash值不能逆推出原文
  • 值不同,Hash值不同。
  • 尽可能分散。
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/318808.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号