面试官问:HashMap在并发状况下为何形成死循环?一脸懵

这个问题是在面试时常问的几个问题,通常在问这个问题以前会问Hashmap和HashTable的区别?面试者通常会回答:hashtable是线程安全的,hashmap是线程不安全的。java

那么面试官就会紧接着问道,为何hashmap不是线程安全的,会形成什么问题么?因而面试者就回答:HashMap在并发状况下的put操做会形成死循环。面试

这时候就会被面试官问:HashMap在并发为何形成死循环?安全

不少面试者这时候就会一脸懵。没有过相关经验和深刻的理解源码是很难回答这个问题的。多线程

下面咱们就经过HahMap源码来验证下,多线程并发put操做为什么会生成环形链表,产生死循环。并发

这是HashMap扩容的源码spa

/**
 * Transfers all entries from current table to newTable. 
 */
void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    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;
        } // while  

    }
}

开始以前先回顾一下HashMap的扩容机制:
HashMap默认设定的装载因子为0.75(可改),HashMap的大小为length,已经装载的元素数量为num,当( num / length )> 装载因子时,
开始扩容线程

先建立一个散列表HashMap:Map<Integer> map = new HashMap<Integer>(2); ,装载因子默认0.75,当插入第二个元素时,会发生扩容
咱们先在map中放入六、8两个元素。设计

插入后的状态

这时有两个线程都执行put操做,那么在此刻两个线程都对HashMap进行扩容,这时候就注意在上文的源码里注释为(关键代码)这一行:Entry<K,V> next = e.next;3d

假如两个线程分别为A、B两个线程。A线程在执行到关键代码这一行线程就被挂起,那么此刻A线程中:e = 6; next = 8;code

接着B线程开始进行扩容,假设新的散列表中,节点6 和 节点8 仍是会产生散列冲突,那么线程B的扩容过程为:

  • 先申请一个空间为旧散列表两倍大的空间

    <img src="https://upload-images.jianshu.io/upload_images/2710833-091c030487692445.png" alt="申请两倍大小的空间" style="zoom:33%;" />

  • 将节点6 迁移至新散列表

节点6迁移至新散列表

  • 将节点8 迁移至新散列表

将节点8 迁移至新散列表

此时线程B的扩容已经完成,节点8 的后继节点为节点6 ,节点6的后继节点为null。

咱们将新旧两个散列表作个对比:

对比

回顾一下线程A的当前状态:e = 6; next = 8;,处于挂起状态。接着A线程取消挂起状态,接着执行(关键代码)以后的代码:将e = 6;节点迁移至新的散列表,并将next = 8的节点赋值给e。扩容并迁移节点6后的状态,以下图所示:

A线程扩容迁移节点6

因而第二次执行while循环时,当前待处理节点:e = 8;

在执行(关键代码)这一行时,因为线程B在扩容时将节点8的后继节点变为节点6,因此next不是为null,而是next = 6;

接着开始执行第三次while循环,因为节点6的后继节点为null,因此 next = null;,执行完第三次while循环的结果为:

循环结束。

能够看到扩容后的散列表中链表成环,若是这时候执行get()方法查询,就会致使死循环。

总结

HashMap的方法不是线程安全的。HashMap在并发执行put操做时发生扩容,可能会致使节点丢失,产生环形链表等状况。

  • 节点丢失,会致使数据不许
  • 生成环形链表,会致使get()方法死循环。

知识拓展

在jdk1.7中,因为扩容时使用头插法,在并发时可能会造成环状列表,致使死循环,在jdk1.8中改成尾插法,能够避免这种问题,可是依然避免不了节点丢失的问题。

建议

HashMap的设计初衷就不是在并发状况下使用,若是有并发的场景,推荐使用ConcurrentHashMap

关注公众号:java之旅

相关文章
相关标签/搜索