HashMap作为老生常谈的问题,备受面试官的青睐,甚至成为了面试必问的问题。由于大量的针对HashMap的解析横空出世,面试官对HashMap的要求越来越高,就像面试官对JVM掌握要求越来越高一样,今天我们来研究下HashMap的链表环化的问题,你知道其中的原理嘛?关注公众号“程序员清辞”,获取更多内容

在JDK1.7版本下,有个线程安全的问题,经常会被问到,很多求职者可能还在对比Hashtable线程安全性,其实面试官想得到的链表成环造成线程安全的问题,而这个问题在JDK1.8中已经得到了解决,但至于出现这样问题的原因,我翻看了很多帖子,大家剖析的很透彻,但是很难理解,今天结合自己的研究利用一篇帖子来阐述其中的奥秘。

JDK1.7扩容源码解析

首先我来了解下HashMap中经典的扩容代码,回顾下扩容的过程

  1. public class HashMap<K,V> extends AbstractMap<K,V>
  2. implements Map<K,V>, Cloneable, Serializable {
  3.    
  4.    //......
  5.    
  6.    // 扩容方法
  7.    void resize(int newCapacity) {
  8.        // 1、创建临时变量,将HashMap数组数据赋值给新数组作临时存储
  9.        Entry[] oldTable = table;
  10.        // 2、判断老数组长度是否超过了允许的最大长度,最大长度为 1 << 30
  11.        int oldCapacity = oldTable.length;
  12.        if (oldCapacity == MAXIMUM_CAPACITY) {
  13.            threshold = Integer.MAX_VALUE;
  14.            return;
  15.       }
  16. // 3、创建新的Entry数组,并扩容
  17.        Entry[] newTable = new Entry[newCapacity];
  18.        // 4、扩容赋值,即将老数组中的数据赋值到新数组中
  19.        // initHashSeedAsNeeded(newCapacity) 得到的是一个hash的随机值(哈希种子),
  20. //在计算哈希码时会用到这个种子,作用是减少哈希碰撞
  21.        transfer(newTable, initHashSeedAsNeeded(newCapacity));
  22.        // 6、扩容后赋值
  23.        table = newTable;
  24.        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
  25.   }
  26.    
  27.    // newTable : 表示新数组,即扩容后创建的新数组
  28.    // rehash : 是否需要重新计算哈希值
  29.    void transfer(Entry[] newTable, boolean rehash) {
  30. int newCapacity = newTable.length;
  31.        // 5、将老map中的数据赋值到新map中(数组和链表复制迁移)
  32. for (Entry<K,V> e : table) {  
  33. while(null != e) {
  34. Entry<K,V> next = e.next;
  35. if (rehash) {
  36. e.hash = null == e.key ? 0 :hash(e.key);
  37. }
  38.                // 计算Entry元素在Entry[]数组中的位置
  39. int i = indexFor(e.hash, newCapacity);

  40. // 链表头插法赋值过程
  41. e.next = newTable[i];
  42. newTable[i] = e;
  43. e = next;
  44. }
  45. }
  46. }
  47.    
  48.    //......
  49.    
  50. }
  1. 创建临时变量,将HashMap数组数据赋值给新数组作临时存储
  2. 判断老数组长度是否超过了允许的最大长度,最大长度为 1 << 30
  3. 创建新的Entry数组,并扩容
  4. 扩容赋值,即将老数组中的数据赋值到新数组中
  5. 将老map中的数据赋值到新map中(数组和链表复制迁移)
  6. 扩容后赋值

链表迁移过程

以下三行代码描述了链表头插的整个过程,下面来剖析下这个过程:

  1. e.next = newTable[i];
  2. newTable[i] = e;
  3. e = next;

关注公众号“程序员清辞”,获取更多内容

假设HashMap的存储状态如下:

关注公众号“程序员清辞”,获取更多内容

e为数组位置的元素,e1、e2为e下形成的链表,h为将要赋值的位置,箭头代表链表指向

e.next = newTable[i]

对oldTable进行遍历的过程中,取出元素e,假设先取出图中的元素e,在执行这行代码时,相当于断开x位置e与e1的链表关系,并与newTable[i]建立链表关系,此时newTable[i]位置为null

newTable[i] = e

此时将oldTable中的e复制到newTable中的i位置,同时链表e指向null

问题:那oldTable中e1和e2形成的链表怎么办?

其实在之前的代码中已经阐述了,详情如下:

  1. while(null != e) {
  2. // 这里已经将e.next存储为一个临时变量,也就是e1和e2形成的链表
  3. Entry<K,V> next = e.next;
  4. if (rehash) {
  5. e.hash = null == e.key ? 0 :hash(e.key);
  6. }
  7. ......
  8. }

e = next;

将next的值赋值给e,这行代码对上述的链表没有实质的影响,并且这已经是while循环的最后一行代码了,这行代码的目的是为下一次while遍历过程能从e1元素开始,而不是e,因为此时需要的遍历的e已经变成了e1。

通过这次数据迁移可能没有得到比较有参考意义的分析,所以我们需要再进行一次遍历分析,而这次遍历分析从e1开始。这里就不详细阐述,直接上图。

e.next = newTable[i]

newTable[i] = e

最终效果

以上就是整个数据迁移的过程,通过链表实例大家发现HashMap利用头插法完成迁移的过程,下面进入重点,链表成环

并发操作链表成环

产生基本条件

  1. 多线程环境并发操作
  2. HashMap扩容时候发生

问题解析

在多线程环境下,a,b两个线程同时操作这个HashMap,由于HashMap是线程不安全的,假如线程a已经完成以上全过程,也就是下图

代码执行到如下位置,还没有完全的出栈

此时线程b同时也在遍历这条链表,同时代码运行到while循环位置

关注公众号“程序员清辞”,获取更多内容

这时线程b已经重新获取e数据时,由于a线程的操作还没有将数据同步到主内存,导致出现如下情况:

问题总结

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

面试中HashMap链表成环的问题你答出了吗的更多相关文章

  1. 大多数人不知道的:HashMap链表成环的原因和解决方案

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

  2. 面试大总结:Java搞定面试中的链表题目总结

    package LinkedListSummary; import java.util.HashMap; import java.util.Stack; /** * http://blog.csdn. ...

  3. (转)面试大总结之一:Java搞定面试中的链表题目

    面试大总结之一:Java搞定面试中的链表题目 分类: Algorithm Interview2013-11-16 05:53 11628人阅读 评论(40) 收藏 举报 链表是面试中常出现的一类题目, ...

  4. 一篇文章搞定面试中的链表题目(java实现)

    最近总结了一下数据结构和算法的题目,这是第二篇文章,关于链表的,第一篇文章关于二叉树的参见 废话少说,上链表的数据结构 class ListNode { ListNode next; int val; ...

  5. 面试中的Java链表

    链表作为常考的面试题,并且本身比较灵活,对指针的应用较多.本文对常见的链表面试题Java实现做了整理. 链表节点定义如下: static class Node { int num; Node next ...

  6. java面试之Hashmap

    在java面试中hashMap应该说一个必考的题目,而且HashMap 和 HashSet 是 Java Collection Framework 的两个重要成员,其中 HashMap 是 Map 接 ...

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

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

  8. 面试中关于Java你所需知道的的一切

    本篇文章会对面试中常遇到的Java技术点进行全面深入的总结,帮助我们在面试中更加得心应手,不参加面试的同学也能够借此机会梳理一下自己的知识体系,进行查漏补缺. 1. Java中的原始数据类型都有哪些, ...

  9. Java中HashMap等的实现要点浅析

    @南柯梦博客中的系列文章对Jdk中常用容器类ArrayList.LinkedList.HashMap.HashSet等的实现原理以代码注释的方式给予了说明(详见http://www.cnblogs.c ...

随机推荐

  1. 一款直击痛点的优秀http框架,让我超高效率完成了和第三方接口的对接

    1.背景 因为业务关系,要和许多不同第三方公司进行对接.这些服务商都提供基于http的api.但是每家公司提供api具体细节差别很大.有的基于RESTFUL规范,有的基于传统的http规范:有的需要在 ...

  2. 乌班图16 配置nginx

    阿里云 乌班图16 安装ngnix sudo apt install nginx nginx 启动 重启 关闭 sudo service nginx start restart stop status ...

  3. web自动化 -- js操作(滑动屏幕、修改页面)

    一.selenium对  js  的操作方法 1.先定义  js 操作   或者  定义  目标元素 2.执行  js  操作:  driver.execute_script(js操作)    或者  ...

  4. BUUCTF-web ZJCTF,不过如此

    很明显要利用伪协议读next.php base64解码后查看源码 <?php $id = $_GET['id']; $_SESSION['id'] = $id; function complex ...

  5. Android上传图片的两种方式

    参考:https://www.jianshu.com/p/f47943880cea

  6. 官宣!AWS Athena正式可查询Apache Hudi数据集

    1. 引入 Apache Hudi是一个开源的增量数据处理框架,提供了行级insert.update.upsert.delete的细粒度处理能力(Upsert表示如果数据集中存在记录就更新:否则插入) ...

  7. 构建私有的verdaccio npm服务

    用了很长一段时间的cnpmjs做库私有库,发现两个问题 1. 最开始是mysql对表情emoij的支持不好,但由于数据库没办法调整所以只好把第三方库都清掉,只留私有库 2. mac 上面cnpm in ...

  8. 安装fiddler 谷歌插件

    移动 .crx 插件无法安装问题 解决方案: 修改后缀名为 .zip 文件 进行解压后,使用浏览器扩展程序加载已解压的文件进行扩展 添加插件 2020-06-20

  9. Python while 中简单的语句组

    Python while 中简单的语句组: 只使用 while: # 简单的语句组 a = 4 b = 8 num = 0 while a < b: print("a 比 b 小&qu ...

  10. PHP strrpos() 函数

    实例 查找 "php" 在字符串中最后一次出现的位置: <?php高佣联盟 www.cgewang.comecho strrpos("I love php, I l ...