1.Jdk1.7之前,hashmap就是链表+数组;Jdk1.8之后hashmap的数据结构变成了红黑树,数组和链表。(很大程度上解决了hash冲突)
2.在JDK1.8中当单个链表的长度超过8的时候,在扩容时会自动转换成红黑树的数据结构;当红黑树的节点小于6个的时候,数据结构会从红黑树变成链表;


(为什么选用红黑树:提高查询效率;极端情况下查询效率也不会太慢;t频繁的插入和删除操作中,红黑树的总体性能更优;提高空间效率)
3. HashMap 基于 Hash 算法实现的
- 当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数 组中的下标
- 存储时,如果出现hash值相同的key,此时有两种情况。
- 如果key相同,则覆盖原始值;
- 如果key不同(出现冲突),则将当前的key-value放入链表中
- 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值
4.hashmap的扩容机制
扩容就是HashMap底层的resize()方法 , 在两种情况下会执行 :
- 初始化HashMap集合, 第一次往里面存元素的时候会执行resize()方法 , 初始化底层的数组(默认容量是16) , 如果构造方法中传入了初始容量参数, 那么初始长度为大于传入初始容量的第一个2的n次幂
- 当HashMap集合中存入的键值对数量超过阈值(0.75)的时候会进行扩容 , 每次扩容容量为之前的2倍
在1.7 中,扩容之后需要重新去计算其Hash值,根据Hash值对节点位置进行重新分配
在1.8版本中,则是根据在同一个桶的位置中进行判断(e.hash & oldCap)是否为0,如果为0 则留在当前位置不动 , 如果不为0 则要移动到原始位置+增加的数组大小这个位置上 , 这也是为什么HashMap扩容容量为2的N次幂的一个原因
为什么HashMap的数组长度一定是2的次幂?
- 计算索引时效率更高:如果是 2 的 n 次幂可以使用位与运算代替取模运算, 效率更高
- 扩容时重新计算索引效率更高: hash & oldCap == 0 的元素留在原来位置 ,否则新位置 = 旧位置 + oldCap
为什么扩容阈值设置为0.75 ?
- 选择扩容阈值为0.75的原因是为了在空间占用与查询时间之间取得较好的权衡
- 大于这个值,空间节省了,但链表就会比较长影响性能
- 小于这个值,冲突减少了,但扩容就会更频繁,空间占用多
5.多线程下Hash Map会有什么问题?
在jdk1.7中,Hashmap在多线程操作过程中可能存在死循环的情况,因为在jdk1.7中hashmap是数组加链表,采用的是头插法。
举个例子:线程A读取到当前的hashmap数据,数据中一个链表,在准备扩容时, 此时线程B介入,他也同时对这个map进行扩容操作。因为是头插法,链表会倒过来,由原来的AB变成BA,扩容完成后线程B退出,执行结束。此时线程A继续执行,这个时候A线程会将A插入新的链表,然后B插入链头,由于线程A的原因,B的next是指向A,即B->A->B,形成了死循环;
具体解析原因:
https://segmentfault.com/a/1190000024510131
在jdk1.8中引入了尾插法,来防止死循环的发生,但是还是会导致数据丢失。
具体解析原因:

为了避免这种情况,我们一般使用线程安全的concurrentHashMap;
6.hashmap的hash函数设计是怎么设计?
hash 函数是先拿到通过 key 的 hashcode,是 32 位的 int 值,然后让 hashcode 的高 16 位和低 16 位进行异或操作。
7.jdk1.7和jdk1.8中都会导致数据丢失
JDK1.7版本和JDK1.8版本都会出现并发数据丢失问题 :
多线程情况下 , 多个线程操作多个key , 多个key的hash值相同 , 在HashMap底层会首先判断该Key对应的索引位置是否存在元素 , 如果不存在元素就会创建新的node , 将node放入到对应的索引位置
这个时候 : 第一个线程判断元素不存在, 创建了node , 在存入到数组之前 , 第二个线程执行了 , 也判断元素不存在, 也会创建node , 之后将元素存入数组 , 这样两个线程都将元素存入到数组的同一个位置, 就会有一个线程的数据丢失

4915

被折叠的 条评论
为什么被折叠?



