引导语

在 JDK7 版本下,很多人都知道 HashMap 会有链表成环的问题,但大多数人只知道,是多线程引起的,至于具体细节的原因,和 JDK8 中如何解决这个问题,很少有人说的清楚,百度也几乎看不懂,本文就和大家聊清楚两个问题:1:JDK7 中 HashMap 成环原因,2:JDK8 中是如何解决的。

JDK7 中 HashMap 成环原因

成环的时机

1:HashMap 扩容时。

2:多线程环境下。

成环的具体代码位置

在扩容的 transfer 方法里面,有三行关键的代码,如下:

    void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
//e为空时循环结束
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;
}
}
}

假设原来在数组 1 的下标位置有个链表,链表元素是 a->b->null,现在有两个线程同时执行这个方法,我们先来根据线程 1 的执行情况来分别分析下这三行代码:

  1. e.next = newTable[i];

    newTable 表示新的数组,newTable[i] 表示新数组下标为 i 的值,第一次循环的时候为 null,e 表示原来链表位置的头一个元素,是 a,e.next 是 b,

    e.next = newTable[i] 的意思就是拿出 a 来,并且使 a 的后一个节点是 null,如下图 1 的位置:

  2. newTable[i] = e;

    就是把 a 赋值给新数组下标为 1 的地方,如下图 2 的位置:

  3. e = next;

    next 的值在 while 循环一开始就有了,为:Entry<K,V> next = e.next; 在此处 next 的值就是 b,把 b 赋值给 e,接着下一轮循环。

  4. 从 b 开始下一轮循环,重复 1、2、3,注意此时 e 是 b 了,而 newTable[i] 的值已经不是空了,已经是 a 了,所以 1,2,3 行代码执行下来,b 就会插入到 a 的前面,如下图 3 的位置:



    这个就是线程 1 的插入节奏。

重点来了,假设线程 1 执行到现在的时候,线程 2 也开始执行,线程 2 是从 a 开始执行 1、2、3、4 步,此时数组上面链表已经形成了 b->a->null,线程 2 拿出 a 再次执行 1、2、3、4,就会把 a 放到 b 的前面,大家可以想象一下,结果是如下图的:



从图中可以看出,有两个相同的 a 和两个相同的 b,这就是大家说的成环,自己经过不断 next 最终指向自己。

注意!!!这种解释看似好像很有道理,但实际上是不正确的,网上很多这种解释,这种解释最致命的地方在于 newTable 不是共享的,线程 2 是无法在线程 1 newTable 的基础上再进行迁移数据的,1、2、3 都没有问题,但 4 有问题,最后的结论也是有问题的

因为 newTable 是在扩容方法中新建的局部变量,方法的局部变量线程之间肯定是无法共享的,所以以上解释是有问题的,是错误的。

那么真正的问题出现在那里呢,其实线程 1 完成 1、2、3、4 步后就出现问题了,如下图:

总结一下产生这个问题的原因:

  1. 插入的时候和平时我们追加到尾部的思路是不一致的,是链表的头结点开始循环插入,导致插入的顺序和原来链表的顺序相反的。
  2. table 是共享的,table 里面的元素也是共享的,while 循环都直接修改 table 里面的元素的 next 指向,导致指向混乱。

接下来我们来看下 JDK8 是怎么解决这个问题。

JDK8 中解决方案

JDK 8 中扩容时,已经没有 JDK7 中的 transfer 方法了,而是自己重新写了扩容方法,叫做 resize,链表从老数组拷贝到新数组时的代码如下:

                    //规避了8版本以下的成环问题
else { // preserve order
// loHead 表示老值,老值的意思是扩容后,该链表中计算出索引位置不变的元素
// hiHead 表示新值,新值的意思是扩容后,计算出索引位置发生变化的元素
// 举个例子,数组大小是 8 ,在数组索引位置是 1 的地方挂着一个链表,链表有两个值,两个值的 hashcode 分别是是9和33。
// 当数组发生扩容时,新数组的大小是 16,此时 hashcode 是 33 的值计算出来的数组索引位置仍然是 1,我们称为老值
// hashcode 是 9 的值计算出来的数组索引位置是 9,就发生了变化,我们称为新值。
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
// java 7 是在 while 循环里面,单个计算好数组索引位置后,单个的插入数组中,在多线程情况下,会有成环问题
// java 8 是等链表整个 while 循环结束后,才给数组赋值,所以多线程情况下,也不会成环
do {
next = e.next;
// (e.hash & oldCap) == 0 表示老值链表
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// (e.hash & oldCap) == 0 表示新值链表
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;
}
}

解决办法其实代码中的注释已经说的很清楚了,我们总结一下:

  1. JDK8 是等链表整个 while 循环结束后,才给数组赋值,此时使用局部变量 loHead 和 hiHead 来保存链表的值,因为是局部变量,所以多线程的情况下,肯定是没有问题的。

  2. 为什么有 loHead 和 hiHead 两个新老值来保存链表呢,主要是因为扩容后,链表中的元素的索引位置是可能发生变化的,代码注释中举了一个例子:

    数组大小是 8 ,在数组索引位置是 1 的地方挂着一个链表,链表有两个值,两个值的 hashcode 分别是是 9 和 33。当数组发生扩容时,新数组的大小是 16,此时 hashcode 是 33 的值计算出来的数组索引位置仍然是 1,我们称为老值(loHead),而 hashcode 是 9 的值计算出来的数组索引位置却是 9,不是 1 了,索引位置就发生了变化,我们称为新值(hiHead)。

    大家可以仔细看一下这几行代码,非常巧妙。

总结

本文主要分析了 HashMap 链表成环的原因和解决方案,你学会了吗?

想知道 HashMap 底层数据结构是什么样的么?想了解 ConcurrentHashMap 是如何保证线程安全的么?想阅读更多 JUC 的源码么,请关注我的新课:面试官系统精讲Java源码及大厂真题,带你一起阅读 Java 核心源码,了解更多使用场景,为升职加薪做好准备。

博客主页

新课:面试官系统精讲Java源码及大厂真题

新课:跟我一起学 DDD

大多数人不知道的:HashMap链表成环的原因和解决方案的更多相关文章

  1. 面试中HashMap链表成环的问题你答出了吗

    HashMap作为老生常谈的问题,备受面试官的青睐,甚至成为了面试必问的问题.由于大量的针对HashMap的解析横空出世,面试官对HashMap的要求越来越高,就像面试官对JVM掌握要求越来越高一样, ...

  2. 源码解析JDK1.8-HashMap链表成环的问题解决方案

    前言 上篇文章详解介绍了HashMap在JDK1.7版本中链表成环的原因,今天介绍下JDK1.8针对HashMap线程安全问题的解决方案. jdk1.8 扩容源码解析 public class Has ...

  3. 2020-04-22:谈谈JDK1.8下的HashMap在并发情况下链表成环的过程。(挖)

    福哥答案2020-04-22: jdk1.8下的hashmap采用的是尾插法,不会有链表成环的问题.jdk1.7下采用的头插***有链表成环的问题. hashmap成环原因的代码出现在transfer ...

  4. HashMap 链表插入方式 → 头插为何改成尾插 ?

    开心一刻 一天,楼主在路上碰到了一个很久没见的朋友,找了个餐馆,坐下聊了起来 楼主:在哪上班了 ? 朋友:火葬场啊 楼主:在那上班,一个月多少钱啊 ? 朋友:两万多啊 楼主(不可思议):多少 ? 朋友 ...

  5. 面试官:HashMap死循环形成的原因是什么?

    介绍 HashMap实现原理 之前的文章已经分析了HashMap在JDK1.7的实现,这篇文章就只分析HashMap死循环形成的原因 死循环形成是在扩容转移元素的时候发生的 void resize(i ...

  6. HashMap 链表和红黑树的转换

    HashMap在jdk1.8之后引入了红黑树的概念,表示若桶中链表元素超过8时,会自动转化成红黑树:若桶中元素小于等于6时,树结构还原成链表形式. 原因: 红黑树的平均查找长度是log(n),长度为8 ...

  7. 玩转Windows服务系列——无COM接口Windows服务启动失败原因及解决方案

    将VS创建的Windows服务项目编译生成的程序,通过命令行 “服务.exe -Service”注册为Windows服务后,就可以通过服务管理器进行管理了. 问题 通过服务管理器进行启动的时候,发现服 ...

  8. PowerDesigner16.5 连64位MySQL,出错:SQLSTATE = IM014。原因及解决方案

  9. NIOS II CPU复位异常的原因及解决方案

    NIOS II CPU复位异常的原因及解决方案   近期在用nios ii做项目时,发现一个奇怪的现象,在NIOS II EDS软件中编写好的代码,烧写到芯片中,第一次能够正常运行,但是当我按下板卡上 ...

随机推荐

  1. 记一次python时间格式转换遇到的坑

    需求:拿到指定格式的时间的前一天的时间,如果今天是月初,年初,自动转换,比如:输入时间是:2019-06-27 23:59:59输出时间是:2019-06-26 23:59:59 之前用datetim ...

  2. 01-Spring Security框架学习

    目录 01-Spring Security框架学习 简介 Spring Security 是什么 Spring Security 解决那些问题 Spring Security 的优点 历史背景 Spr ...

  3. Cobbler 自动安装CentOS7

    1. Cobbler介绍 Cobbler是一个Linux服务器安装的服务,可以通过网络启动(PXE)的方式来快速安装.重装物理服务器和虚拟机,同时还可以管理DHCP,DNS等.Cobbler可以使用命 ...

  4. Cocos经典游戏教程之仿皇室战争

    版权声明: 本文原创发布于博客园"优梦创客"的博客空间(网址:http://www.cnblogs.com/raymondking123/)以及微信公众号"优梦创客&qu ...

  5. JVM总结(一)

    JVM总结(1) 1.JVM组成: JVM由类加载器子系统.运行时数据区.执行引擎以及本地方法接口组成. 2.JVM运行原理: Java源文件经编译器,编译成字节码程序,通过JVM将每一条指令翻译成不 ...

  6. java学习-NIO(三)Channel

    通道(Channel)是java.nio的第二个主要创新.它们既不是一个扩展也不是一项增强,而是全新.极好的Java I/O示例,提供与I/O服务的直接连接.Channel用于在字节缓冲区和位于通道另 ...

  7. 【Kubernetes 系列五】在 AWS 中使用 Kubernetes:EKS

    目录 1. 概述 2. 版本 3. 预备 3.1. 操作环境 3.2. 角色权限 3.2.1. CloudFormation 完全权限 3.2.2. EKS 读写权限 3.2.3. EC2 相关权限 ...

  8. IDEA搭建工程

    1. 创建一个Project File -> New -> Project...   : 选择jdk版本,然后Next: 输入项目名,确定项目路径,Finish. 2. 创建一个Modul ...

  9. forward(转发)和redirect(重定向)的区别

    在学习Servlet和JSP时,经常会使用到forward和redirect,我们先来看这两者在Servlet中的调用方式: 1.forward request.getRequestDispatche ...

  10. 在Win10下,python3和python2同时安装并解决pip共存问题

    前提 本文是在Windows64位系统下进行的,32位系统请下载相应版本的安装包,安装方法类似. 在Win10下,python3和python2同时安装并解决pip共存问题解决: 1.下载python ...