HashMap多线程扩容导致死循环解析(JDK1.7)

前言

前一篇 HashMap底层结构与实现原理 遗留了一个问题:JDK1.7中的HashMap在多线程情况下扩容可能会导致死循环。本篇就这个问题进行讲解。

扩容死循环

前一篇深入的讲解了HashMap1.7扩容的过程,这里回顾一下在扩容过程中,单链表的表现,相关的代码如下

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    // 外层循环遍历数组槽(slot)
    for (Entry<K,V> e : table) {
    	// 内层循环遍历单链表
        while(null != e) {
        	// 记录当前节点的next节点
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            // 找到元素在新数组中的槽(slot)
            int i = indexFor(e.hash, newCapacity);
            // 用头插法将元素插入新的数组
            e.next = newTable[i];
            newTable[i] = e;
            // 遍历下一个节点
            e = next;
        }
    }
}

单线程情况下,假设A、B、C三个节点处在一个链表上,扩容后依然处在一个链表上,代码执行过程如下:
JDK1.7-HashMap扩容时链表转移过程
需要注意的几点是

  • 单链表在转移的过程中会被反转
  • table是线程共享的,而newTable是不共享的
  • 执行table = newTable后,其他线程就可以看到转移线程转移后的结果了

理解了单线程下链表在扩容时的行为,再来看多线程的情况就比较容易了

此处感谢评论区@伤神v同学的指点,以下多线程扩容图是修正后的图

还是关注transfer方法这段代码

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;  // *线程1在这行暂停(尚未执行)
            e = next;
        }
    }
}

HashMap扩容死循环

  • 线程1执行newTable[i] = e时暂停(未执行)
  • 线程2直接扩容完成
  • 线程1继续执行,此时线程1可以看到线程2扩容后的结果

图中已经画出了每一行代码执行后,HashMap的结构图,仔细观察图中的结构变化,就能理解为什么会死循环。

由此,完完整整的解释了为什么多线程情况下,JDK1.7版本的HashMap扩容有可能出现死循环。

JDK1.8改进

JDK1.8中扩容的方法是resize,对应的代码是(HashMap中第715行至第742行):

// 低位链表头节点,尾结点
// 低位链表就是扩容前后,所处的槽(slot)的下标不变
// 如果扩容前处于table[n],扩容后还是处于table[n]
Node<K,V> loHead = null, loTail = null;
// 高位链表头节点,尾结点
// 高位链表就是扩容后所处槽(slot)的下标 = 原来的下标 + 新容量的一半
// 如果扩容前处于table[n],扩容后处于table[n + newCapacity / 2]
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
    next = e.next;
    if ((e.hash & oldCap) == 0) {
        if (loTail == null)
            loHead = e;
        else
            loTail.next = e;
        loTail = e;
    }
    else {
        if (hiTail == null)
            hiHead = e;
        else
            hiTail.next = e;
        hiTail = e;
    }
} while ((e = next) != null);
if (loTail != null) {
    loTail.next = null;
    // 低位链表在扩容后,所处槽的下标不变
    newTab[j] = loHead;
}
if (hiTail != null) {
    hiTail.next = null;
    // 高位链表在扩容后,所处槽的下标 = 原来的下标 + 扩容前的容量(也就是扩容后容量的一半)
    newTab[j + oldCap] = hiHead;
}

注意第12行的代码(e.hash & oldCap) == 0就可以判断,当前槽上的链表在扩容前和扩容后,所在的槽(slot)下标是否一致。举个例子:
假如一个key的hash值为1001 1100,转换成十进制就是156,数组长度为1000,转换成十进制就是8。

  1001 1100
& 0000 1000
--------------
  0000 1000

也就是(e.hash & oldCap) != 0,很容易计算出,扩容前这个key的下标是4(156 % 8 = 4),扩容后下标是12(156 % 16 = 12)即:12 = 4 + 16 / 2,满足n = n + newCapacity / 2,由此可以看出这种计算方式非常巧妙。至于第12行之后的代码就是基本的单链表操作了,只是一个单链表同时具有头指针尾指针,等到链表被分成高位链表和低位链表后,再一次性转移到新的table。这样就完成了单链表在扩容过程中的转移,使用两条链表的好处就是转移前后的链表不会倒置,更不会因为多线程扩容而导致死循环。

总结

本篇主要通过图解的方式,解释了为什么JDK1.7中的HashMap在多线程情况下扩容可能死循环,也解释了JDK1.8如何解决这个问题。不得不说,画图是个很好的分析方式,根据代码,一步一步把结构图画出来,比对着代码瞎琢磨效果好多了。

以上就是本篇文章的全部内容。

  • 10
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
hashmap的底层原理 hashmap产生锁的原因 hashmap的容量为什么一定要是2的幂呢 TreeMap的底层原理 HashMap,Hashtable和ConcurrentHashMap的区别 在ArrayList和LinkedList尾部添加元素,谁的效率更高 如果HashMap或者hashTable的key是一个自定义的类该怎么办 为什么重写equals还要重写hashCode? 介绍一下volatile jdk1.5新特性 jdk1.7新特性 jdk1.8新特性 java语言有哪些优点? 同一个.java文件中是否可以有多个main方法 一个".java"源文件中是否可以包括多个类(不是内部类)?有什么限制? 如何在main方法执行前输出”hello world” java程序的初始化顺序 请说出作用域public,private,protected,以及不写时的区别 为什么java中有些接口没有任何方法 java中的clone方法有什么作用 面向对象有哪些特征 面向对象的开发方式有什么优点 组合和继承有什么区别 多态的实现机制是什么 重写和重载的区别 抽象类和接口有什么区别 break如果跳出多重循环 final,finally和finalize有什么区别 使用switch时有哪些注意事项 instanceof有什么作用 什么是不可变类 类型转换 Math类的round,ceil和floor方法 值传递和引用传递有什么不同? char型变量是否可以存储一个中文汉字 s=null和s=” ”是否相同 new String(“abc”)创建了几个对象 String变量做“+”运算时的编译优化 "=="和equals方法究竟有什么区别 String,StringBuffer,StringBuilder有什么区别 java中的数组是不是对象 数组初始化的方式有哪几种 length属性和length()方法有什么区别 finally块中的代码什么时候被执行 Java中的异常处理机制的简单原理和应用。 java socket java序列化 JVM加载class文件的原理 双亲委派模型 为什么要自定义类加载器 如何自定义类加载器 什么是GC 内存泄漏和内存溢出 Java的内存模型(JVM的内存划分) JVM内存模型1.7和1.8的区别 如何判断一个对象是否是垃圾对象 垃圾回收算法 Minor GC和Full GC 垃圾收集器 集合的继承体系 Collection 和 Collections的区别。 如何通过jdbc访问数据库 JDBC处理事务采用什么方法 Statement和PreparedStatement的区别 getString()方法和getObject()方法有什么区别 jdbc和hibernate有什么区别 http1.0和http1.1和http2.0的区别 http和https的区别 http缓存 cookie和session forward与redirect区别 页面请求的工作流程 HTTP中的GET和POST方法有什么区别 什么是Servlet servlet是线程安全的吗 Servlet的生命周期 JSP和Servlet的区别和联系 什么是MVC模型 TCP的连接和释放过程 什么是长连接和短连接 长连接是如何实现的 计算机网络体系结构 谈谈你对Struts的理解。 谈谈你对Hibernate的理解。 谈谈你对Lucene和solr的理解 谈谈你对ActiveMQ的理解 Spring的IOC,DI和AOP 谈谈你对webservice和dubbo的理解 谈谈你的SOA的理解。 谈谈你对freemarker的理解 谈谈你对springMVC的理解 谈谈你对mybatis的理解 hibernate和mybatis的区别 同步接口和异步接口的区别 为什么要加入锁机制 如何确定是否应该上锁?即如何判断是否有线程安全问题 什么是markword 乐观锁和悲观锁 偏向锁 轻量级锁 自旋锁 自适应自旋锁 重量级锁 synchronized 可重入锁 土方法实现可重入锁 使用AQS类实现可重入锁 CAS MySQL 中的行级锁、表级锁和页级锁 java中的锁 公平锁和非公平锁 锁的总结 锁的优化 .......... 还有好多,不想写了,太多了,都是题主呕心沥血总结的

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值