HashMap 线程不安全相关问题
Posted 码农每日一题
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HashMap 线程不安全相关问题相关的知识,希望对你有一定的参考价值。
码农每日一题
长按关注置顶
工作日每天推送一个短小精干的技术知识点,让您可以随时查漏补缺。
问:简单说说 HashMap 为什么是线程不安全的?具体体现在哪些方面?
答:对于 JDK1.7 和 JDK1.8 的 HashMap 中迭代器的 fail-fast 策略导致了并发不安全,即如果在使用迭代器的过程中有其他线程修改了 HashMap 就会抛出 ConcurrentModificationException 异常(fail-fast 策略),具体可以参考以前的一篇推文《》。
对于 JDK1.7 的 HashMap 并发 put 操作触发扩容导致潜在可能的死循环现象,而 JDK1.8 的 HashMap 并发 put 操作不会导致潜在的死循环。前面我们已经通过《》和《》问题了解到了 HashMap 扩容操作的核心流程,对于 JDK1.7 来说哈希冲突的链表结构在扩容前后会进行一次逆向首尾对调操作,而对于 JDK1.8 来说扩容前后链表顺序性不变,所以对于 JDK1.7 扩容的核心代码如下:
void transfer(HashMapEntry[] newTable) {
//扩容新数组的容量
int newCapacity = newTable.length;
//遍历旧数组index元素
for (HashMapEntry<K,V> e : table) {
//对应index数组位置上哈希冲突的链表元素逆向颠倒
while(null != e) {
HashMapEntry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
接着我们模拟并发 put 操作(为了简单我们假设扩容后链表元素在数组的index位置不变),即同一时刻有 Thread-1、Thread-2 进行 put 操作且这次 put 操作恰巧触发扩容操作,假设 Thread-1 线程的操作执行到如图所示语句:
即 Thread-1 进行 put 操作触发了扩容,但是这时候仅仅是准备好了新容量的数组然后进入上面 transfer 方法的 HashMapEntry<K,V> next = e.next; 语句,即此时 e=keyHash(9),next=keyHash(1),然后由于并发导致此时 Thread-1 线程被挂起;此时 Thread-2 线程也在进行 put 操作,假设 Thread-2 线程很顺利的抢占到资源顺利的执行完了 transfer 方法,即如下图所示:
紧接着当 Thread-2 执行完上面时间片段后假设被挂起的 Thread-1 线程得到了执行机会,这时候尴尬的事情就发生了,我们看下 transfer 方法的交换核心代码:
//对于哈希冲突的index上链表进行逆向
while(null != e) {
HashMapEntry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
Thread-1 在第一圈 while 循环时 e 和 next 都是之前挂起时的指向,即 e=keyHash(9)、next=keyHash(1),接着被唤醒后重新计算 index 假设还是 1,然后 e.next =keyHash(1),因为此时 newTable 已经被 Thread-2 进行过扩容重放操作了,然后 newTable[1]=keyHash(9),然后进行循环操作,如下图:
可以看见,当 Thread-1 与 Thread-2 同时进行 put 操作触发扩容出现上面所示场景情况时就可能在扩容后导致链表出现环形,因此当我们接着进行 put 或者 get 操作恰巧又在这个 index 位置时就会出现死循环,源代码如下:
public V put(K key, V value) {
......
int i = indexFor(hash, table.length);
//此循环中 e=e.next 由于前面并发扩容导致的循环链表永远不为null
for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) {
......
}
......
}
public V get(Object key) {
......
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
final Entry<K,V> getEntry(Object key) {
......
//此循环中 e=e.next 由于前面并发扩容导致的循环链表永远不为null
for (HashMapEntry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
......
}
return null;
}
所以在 JDK1.7 中并发扩容操作可能会导致哈希碰撞的链表结构为循环链表,从而导致在后续 put、get 操作时发生死循环。而对于 JDK1.8 中扩容链表的顺序是不会发生逆向的,所以自然怎么遍历都不会出现循环链表的情况,故 JDK1.8 中不会出现并发循环链表,但由于 JDK1.7 与 JDK1.8 中都是无锁保护的,所以依然是并发不安全的。
HashMap 系列已推送历史~
《》
《》
《》
《》
《》
放松一下,顺带评论点赞分享一波~
以上是关于HashMap 线程不安全相关问题的主要内容,如果未能解决你的问题,请参考以下文章
面试官:小伙子,你给我说一下HashMap 为什么线程不安全?
面试官:小伙子,你给我说一下HashMap 为什么线程不安全?
面试官:小伙子,你给我说一下HashMap 为什么线程不安全?