hashmap是单向链表吗_面试中HashMap链表成环的问题你答出了吗

HashMap作为老生常谈的问题,备受面试官的青睐,甚至成为了面试必问的问题。由于大量的针对HashMap的解析横空出世,面试官对HashMap的要求越来越高,就像面试官对JVM掌握要求越来越高一样,今天我们来研究下HashMap的链表环化的问题,你知道其中的原理嘛?

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

JDK1.7扩容源码解析

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

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {        //......        // 扩容方法    void resize(int newCapacity) {        // 1、创建临时变量,将HashMap数组数据赋值给新数组作临时存储        Entry[] oldTable = table;        // 2、判断老数组长度是否超过了允许的最大长度,最大长度为 1 << 30        int oldCapacity = oldTable.length;        if (oldCapacity == MAXIMUM_CAPACITY) {            threshold = Integer.MAX_VALUE;            return;        }  // 3、创建新的Entry数组,并扩容        Entry[] newTable = new Entry[newCapacity];        // 4、扩容赋值,即将老数组中的数据赋值到新数组中        // initHashSeedAsNeeded(newCapacity) 得到的是一个hash的随机值(哈希种子),在计算哈希码时会用到这个种子,作用是减少哈希碰撞        transfer(newTable, initHashSeedAsNeeded(newCapacity));        // 6、扩容后赋值        table = newTable;        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);    }        // newTable : 表示新数组,即扩容后创建的新数组    // rehash :是否需要重新计算哈希值    void transfer(Entry[] newTable, boolean rehash) {  int newCapacity = newTable.length;        // 5、将老map中的数据赋值到新map中(数组和链表复制迁移)  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);    }                // 计算Entry元素在Entry[]数组中的位置    int i = indexFor(e.hash, newCapacity);    // 链表头插法赋值过程    e.next = newTable[i];    newTable[i] = e;    e = next;   }  } }        //......    }
  1. 创建临时变量,将HashMap数组数据赋值给新数组作临时存储

  2. 判断老数组长度是否超过了允许的最大长度,最大长度为 1 << 30

  3. 创建新的Entry数组,并扩容

  4. 扩容赋值,即将老数组中的数据赋值到新数组中

  5. 将老map中的数据赋值到新map中(数组和链表复制迁移)

  6. 扩容后赋值

链表迁移过程

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

e.next = newTable[i];newTable[i] = e;e = next;

假设HashMap的存储状态如下:

9aeb145179646f807d5b59174e9e720e.png

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

e.next = newTable[i]

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

4ac051b657680d003818cf0a14ceaeca.png

newTable[i] = e

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

e4d7c1b80a35d17a2482da59f55335ab.png

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

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

while(null != e) { // 这里已经将e.next存储为一个临时变量,也就是e1和e2形成的链表 Entry next = e.next; if (rehash) {  e.hash = null == e.key ? 0 :hash(e.key); } ......}  

e = next;

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

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

e.next = newTable[i]

c713ac68dace737d5bf04f4777897d8d.png

newTable[i] = e

5a5096f66949e691abda88d8d3e9f029.png

最终效果

bbccac89794052377e5429dd9871b575.png

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

并发操作链表成环

产生基本条件

  1. 多线程环境并发操作

  2. HashMap扩容时候发生

问题解析

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

bbccac89794052377e5429dd9871b575.png

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

b16a329add05f4d4c6d557e5ef51934a.png

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

6ebef59dfd77bb3e4336df912102ce70.png

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

cad1d5d8056ce57d6d8efad5295fbd16.png

  1. 插入的时候和平时我们追加到尾部的思路是不一致的,是链表的头结点开始循环插入,导致插入的顺序和原来链表的顺序相反的。

  2. table 是共享的,table 里面的元素也是共享的,while 循环都直接修改 table 里面的元素的 next 指向,导致指向混乱。

面试总结
  1. 插入的时候和平时我们追加到尾部的思路是不一致的,是链表的头结点开始循环插入,导致插入的顺序和原来链表的顺序相反的。

  2. table 是共享的,table 里面的元素也是共享的,while 循环都直接修改 table 里面的元素的 next 指向,导致指向混乱。

END

作者:程序员清辞

微信:qingci8848

695ba9a46b45f8485091cab054b9dbbf.gif

欢迎长按下图关注公众号程序员清辞,收看更多精彩内容

1a33ca16f56540478f6b48a8b401176a.png我就知道你“在看” 30ce313ae32523ba746808681c45359d.gif
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值