@[TOC](`jdk 1.8 hashmap和1.7 hashmap区别 原理解析)
#在jdk中,目前使用比较多的主要是1.8,主要是因为1.8在1.7的基础上优化了很多内容,尤其是hashmap这个集合 在内部结构和实现上都是进行了优化和调整。下面主要讲了1.7发生死循环的原因和过程,以及1.8在添加元素的实现,最后总结了1.7和1.8的主要区别。
part1:hashmap比较
通过jdk的源码我们可以了解到,1.8在1.7的基础上,hashmap做了比较大的改动,下面具体说明:
1.7 hashmap
简单来说:*Entry数组+链表*的结构;插入数据使用*头插法*
我们都知道hashmap是线程不安全的,但是如果被用在了多线程上,1.7的hashma是有可能导致死循环,这是程序绝对不允许的。产生死循环的情况主要是在多线程下,进行put操作的时候,hashmap的Entry 链表有可能形成环形数据结构,一旦形成环形数据结构 Entry 的 next 节点永远不为空,就会产生死循环获取 Entry。
通过源码我们可以了解到,引发死循环,是在 HashMap 的扩容操作中。正常的扩容操作是这个流程。HashMap 的扩容在 put 操作中会触发扩容,主要是三个方法

综合来说,HashMap 一次扩容的过程:
1、取当前 table 的 2 倍作为新 table 的大小
2、根据算出的新 table 的大小 new 出一个新的 Entry 数组来,名为 newTable
3、轮询原 table 的每一个位置,将每个位置上连接的 Entry,算出在新 table上的位置,并以链表形式连接
4、原 table 上的所有 Entry 全部轮询完毕之后,意味着原 table 上面的所有Entry 已经移到了新的 table 上,HashMap 中的 table 指向 newTable
这里用一个实例说明死循环的产生:
【正常情况的扩容】:
现在 hashmap 中有三个元素,Hash 表的 size=2, 所以 key = 3, 7, 5,在 mod 2
以后都冲突在 table[1]这里了

按照源码中扩容的最后一步,把原数组移动到新数组的过程

对 table[1]中的链表来说,进入 while 循环,此时 e=key(3),那么 next=key(7),经过计算重新定位 e=key(3)在新表中的位置,并把 e=key(3)挂在 newTable[3]的位置

这样循环下去,将 table[1]中的链表循环完成后,于是 HashMap 就完成了扩容

【并发下扩容】:
hashmap的初始化还是一样的:

我们现在假设有两个线程并发操作,都进入了扩容操作
我们以颜色进行区分两个线程:

回顾我们的扩容代码,我们假设,线程 1 执行到 Entry<K,V> next = e.next;时被操作系统调度挂起了,而线程 2 执行完成了扩容操作

于是,在线程 1,2 看来,就应该是这个样子

接下来,线程 1 被调度回来执行:

**【注意】:**在内存中仅有一份实例,hashmap的table中拥有的只是这些元素的一份引用
]
开始新一轮while循环,执行Entry<K,V>next = e.next;因为线程2已经将key(7)的next指向了key(3),所以next=key(3);

开始新一轮while循环 指向Entry<K,V> next= e.next;所以next=null

执行 e.next = newTable[i];
nextTable[i]=e后,线程1的table变成结果所示,循环链表产生!
e=


循环列表产生后,一旦线程 1 调用 get(11,15 之类的元素)时,就会进入一个死循环的情况,将 CPU 的消耗到 100%。
【总结】
HashMap 之所以在并发下的扩容造成死循环,是因为,多个线程并发进行时,因为一个线程先期完成了扩容,将原 Map 的链表重新散列到自己的表中,并且链表变成了倒序,后一个线程再扩容时,又进行自己的散列,再次将倒序链表变为正序链表。于是形成了一个环形链表,当 get 表中不存在的元素时,造成死循环。
1.8 hashmap
简单来说:*Node数组+链表&红黑树*的结构;插入数据使用*尾插法*
正因为1.7的hashmap会有死循环的产生,并且有可能数据全部挂到一个链表,导致性能下降。所以1.8做好了很大的改进,首先数据结构上采用了数组+链表&红黑树的结构,当链表长度达到8以后,就会进行红黑树的转化,这样查找性能上有有显著提升;另外在插入数据的时候,采用尾插法,这样不会改变已经移动完成的数据的引用指向,也就不会导致死循环的产生。
我们看源码的put方法:
/**
* Implements Map.put and related methods.
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don’t change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
/*
* Replaces all linked nodes in bin at index for given hash unless
* table is too small, in which case resizes instead.
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
从put方法可以看出:
1.根据key计算出hash值,然后看table是否为空,为空初始化table;不为空,看计算出的hash在table的对应位置上是否有元素,如果没有元素直接把数据插入
2.如果table对应位置上有元素,这个时候,就会判断( else if (p instanceof TreeNode))当前位置是否是树形节点,如果是树形节点,就按照树的方式插入元素
3.如果当前位置是链表节点,就按照链表的方式插入元素,插入完成后,再判断链表长度是否大于8,如果大于8,就进行树化,但是进行树化的时候(treeifyBin),会再判断table的大小大于等于64才会进行树化,否则只是进行扩容
4.元素插入完成后,再判断是否需要扩容
另外remove方法和put方法基本类似,只是在remove的时候,如果是在操作树形节点,执行remove后,会判断节点元素小于等于6的时候,会将树形结构转化为链表。(为什么不是7 -》如果是7的化,map内部 树形和链表之间的转化会比较频繁,从而会影响map的性能)
part2:1.7&&1.8hashmap总结
1.8主要优化减少了Hash冲突 ,提高哈希表的存、取效率。
底层数据结构不一样,1.7是数组+链表,1.8则是数组+链表+红黑树结构(当链表长度大于8,转为红黑树)。
JDK1.8中resize()方法在表为空时,创建表;在表不为空时,扩容;而JDK1.7中resize()方法负责扩容,inflateTable()负责创建表。
1.8中没有区分键为null的情况,而1.7版本中对于键为null的情况调用putForNullKey()方法。但是两个版本中如果键为null,那么调用hash()方法得到的都将是0,所以键为null的元素都始终位于哈希表table【0】中。
当1.8中的桶中元素处于链表的情况,遍历的同时最后如果没有匹配的,直接将节点添加到链表尾部;而1.7在遍历的同时没有添加数据,而是另外调用了addEntry()方法,将节点添加到链表头部。
1.7中新增节点采用头插法,1.8中新增节点采用尾插法。这也是为什么1.8不容易出现环型链表的原因。
1.7中是通过更改hashSeed值修改节点的hash值从而达到rehash时的链表分散,而1.8中键的hash值不会改变,rehash时根据(hash&oldCap)==0将链表分散。
1.8rehash时保证原链表的顺序,而1.7中rehash时有可能改变链表的顺序(头插法导致)。
在扩容的时候:1.7在插入数据之前扩容,而1.8插入数据成功之后扩容。、
【总结】原文链接:https://blog.youkuaiyun.com/h1458280799/article/details/85265135
需要了解concurrenthashmap的朋友详见下一篇文章:
https://blog.youkuaiyun.com/xqqTechnology/article/details/106455305
本文详细分析了JDK 1.7和1.8 HashMap在并发扩容时可能导致死循环的问题,以及1.8版如何通过尾插法和红黑树优化避免该问题。1.8版引入了数组+链表+红黑树的数据结构,减少了Hash冲突,提高了存储和检索效率。在扩容策略上,1.8版选择在插入数据成功后进行扩容,与1.7版不同。
2695

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



