共计 3250 个字符,预计需要花费 9 分钟才能阅读完成。
前言
HashMap 在 JDK7 和 JDK8 是有了一些不同的,具体体现如下:
- JDK7HashMap 底层是数组 + 链表,而 JDK8 是数组 + 链表 + 红黑树
- JDK7 扩容采纳头插法,而 JDK8 采纳尾插法
- JDK7 的 rehash 是全副 rehash,而 JDK8 是局部 rehash。
- JDK8 对于 key 的 hash 值计算相比于 JDK7 来说有所优化。
如果还有趣味的小伙伴能够学习学习我的以下文章,写的非常具体!!
高频考题:手写 HashMap
JDK7、8 扩容源码级详解
JDK7、8HashMap 的 get()、put() 流程详解
JDK7 HashMap
JDK7HashMap 在多线程环境下会呈现死循环问题。
如果此时 A、B 线程同时对一个 HashMap 进行 put 操作,且 HashMap 刚号达到扩容条件须要进行扩容
那么这两个线程都会取对 HahsMap 进行扩容(JDK7HashMap 扩容调用 resize() 办法,而 resize() 办法中须要调用 transfer() 办法将旧数组元素全副 rehash 到新数组中去 == 重点:这里在多线程环境下就会呈现问题 ==)
void resize(int newCapacity) {Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
// 对数组的每一条链表遍历 rehash
for (Entry<K,V> e : table) {while(null != e) {
// 保留下一个节点
Entry<K,V> next = e.next;
if (rehash) {e.hash = null == e.key ? 0 : hash(e.key);
}
// 失去对应在新数组中的索引地位
int i = indexFor(e.hash, newCapacity);
// 尾插法
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
咱们假如当初有一个链表 C——>D, 且 C、D 扩容后计算的索引地位仍然不变,那他么还在同一链表中
当初 A 线程进入到 transfer 办法拿到 C 和它的下一个节点 D(Entry<K,V> next = e.next;) 后,A 线程被挂起,此时 B 线程失常走流程将 C、D rehash 到新的数组中,那么依据头插法在新的数组中是 D——>C
B 执行完之后,A 线程持续去执行
因为 A 获取到了 e = C,next = D, 所以 C 能够进行 rehash,C 进行完后拿到 D,发现 D.next = C, 所以 D 也能够进行 rehash,那么此时因为 D——>C, 此时会再拿到 C,发现 C.next = null,但 C 不是 null,所以 C 再进行 rehash,此时链表尾 C——> D ——>C, 因为此时 e = NULL,所以退出循环,此时呈现死循环。C——>D——>C。
== 各位能够好好想想这些话或者本人在草稿纸上画一画再来看上面的图!==
图示演示:
== B 失常执行实现 ==
== A 继续执行 ==
因为 A 获取到了 e = C,next = D, 所以 C 能够进行 rehash
C 进行完后拿到 e = D,发现 D.next = C, 所以 D 也能够进行 rehash
那么此时因为 D——>C, 此时会再拿到 C,发现 C.next = null,但 C 不是 null,所以 C 再进行 rehash
此时 e = NULL,所以退出循环,此时呈现死循环。C——>D——>C。
JDK8 HashMap
JDK1.8 会呈现数据笼罩的状况
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;
}
- == 第 6 行代码 ==:假如两个线程 A、B 都在进行 put 操作,并且依据 key 计算出的 hash 值雷同,那么失去得索引下标也雷同,当线程 A 执行完第六行代码后因为工夫片耗尽导致被挂起,而线程 B 失去工夫片后在该下标处插入了元素,实现了失常的插入,而后线程 A 取得工夫片,因为之前曾经进行了 hash 碰撞的判断,所有此时不会再进行判断,而是间接进行插入,这就导致了线程 B 插入的数据被线程 A 笼罩了,从而线程不平安。
- == 第 38 行代码 ==++size 不平安,还是线程 A、B,这两个线程同时进行 put 操作时,假如以后 HashMap 的 zise 大小为 10,当线程 A 执行到第 38 行代码时,从主内存中取得 size 的值为 10 后筹备进行 + 1 操作,然而因为工夫片耗尽只好让出 CPU,线程 B 高兴的拿到 CPU 还是从主内存中拿到 size 的值 10 进行 + 1 操作,实现了 put 操作并将 size=11 写回主内存,而后线程 A 再次拿到 CPU 并继续执行 (此时 size 的值仍为 10),当执行完 put 操作后,还是将 size=11 写回内存,此时,线程 A、B 都执行了一次 put 操作,然而 size 的值只减少了 1,所有说还是因为数据笼罩又导致了线程不平安。
<br/>
最初
我是 Code 皮皮虾,一个酷爱分享常识的 皮皮虾爱好者,将来的日子里会不断更新出对大家无益的博文,期待大家的关注!!!
创作不易,如果这篇博文对各位有帮忙,心愿各位小伙伴能够 == 一键三连哦!==,感激反对,咱们下次再见~~~
== 分享纲要 ==
大厂面试题专栏
Java 从入门到入坟学习路线目录索引
开源爬虫实例教程目录索引
<font size=”5″> 更多精彩内容分享,请点击 Hello World (●’◡’●)
欢送关注我的公众号,外延更多优质博文分享,期待您的退出!😁。