【Java & 数据结构】HashMap学习笔记

1. 什么是HashMap

HashMap采用Entry数组来存储key-value对,每一个键值对组成了一个Entry实体,Entry类实际上是一个单向的链表结构,它具有Next指针,可以连接下一个Entry实体。 只是在JDK1.8中,链表长度大于8的时候,链表会转成红黑树!

简单来说:

  • HashMap是一种数据结构,表达得是键值对关系
  • JDK1.7中:数组+链表
    在这里插入图片描述
  • JDK1.8中:数组+链表+红黑树
    在这里插入图片描述

2. 数据结构分析

  • 数组是用来确定桶的位置,利用元素的key的hash值对数组长度取模得到,查找效率O(1);
  • 链表是用来解决hash冲突问题,当出现hash值一样的情形,就在数组上的对应位置形成一条链表,查找效率O(n);
  • 红黑树是用来解决链表太长时查找效率低的问题,红黑树是平衡二叉树的一种,查找效率O(log n)。

3. Put和Get过程

Put过程:

  • 对key的hashCode()做hash运算,计算index;
  • 如果没碰撞直接放到bucket里;
  • 如果碰撞了,以链表的形式存在buckets后;
  • 如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD),就把链表转换成红黑树;
  • 如果节点已经存在就替换old value(保证key的唯一性)
  • 如果bucket满了(超过loadfactor*current capacity),就要resize。

简单来说:按照数组–>链表–>红黑树–>扩容的顺序进行数据插入。

Get过程:

  • 对key的hashCode()做hash运算,计算index;
  • 如果在bucket里的第一个节点里直接命中,则直接返回;
  • 如果有冲突,则通过key.equals(k)去查找对应的Entry。

4. 扩容机制

当发生哈希冲突并且size大于阈值(loadfactor*current capacity)的时候,需要进行数组扩容,扩容时,需要新建一个长度为之前数组2倍的新的数组,然后将当前的Entry数组中的元素全部传输过去(按照Put过程进行),扩容后的新数组长度为之前的2倍,所以扩容相对来说是个耗资源的操作。

为什么扩容是2的次幂?

HashMap的数组长度一定保持2的次幂,比如16的二进制表示为 10000,那么length-1就是15,二进制为01111,同理扩容后的数组长度为32,二进制表示为100000,length-1为31,二进制表示为011111。

  • 保证得到的新的数组索引和老数组索引一致
    在这里插入图片描述
  • 会使得获得的数组索引index更加均匀
    在这里插入图片描述
  • 使得低位更加散列,任何一位的变化都会对结果产生影响
    在这里插入图片描述
  • 减小冲突概率,提高资源利用率
    如果不是2的次幂,也就是低位不是全为1此时,要使得index=21,h的低位部分不再具有唯一性了,哈希冲突的几率会变的更大,同时,index对应的这个bit位无论如何不会等于1了,而对应的那些数组位置也就被白白浪费了。

5. JAVA HASHMAP的死循环

简单而言
HashMap不是线程安全的,若是并发的进行ReHash,可能因为种种巧合,使得在链表中出现环形链接,一旦进行Get操作,将陷入死循环。以下是来自coolshell的一个例子,详情请阅读原文。

正常的ReHash的过程

  • 我假设了我们的hash算法就是简单的用key mod 一下表的大小(也就是数组的长度)
  • 最上面的是old hash表,其中的Hash表的size=2, 所以key = 3, 7, 5,在mod 2以后都冲突在table[1]这里了。
  • 接下来的三个步骤是Hash表 resize成4,然后所有的<key,value> 重新rehash的过程
    在这里插入图片描述

并发下的Rehash
1)假设我们有两个线程。而我们的线程二执行完成了。于是我们有下面的这个样子。
在这里插入图片描述
注意,因为Thread1的 e 指向了key(3),而next指向了key(7),其在线程二rehash后,指向了线程二重组后的链表。我们可以看到链表的顺序被反转后。

2)线程一被调度回来执行。

先是执行 newTalbe[i] = e;
然后是e = next,导致了e指向了key(7),
而下一次循环的next = e.next导致了next指向了key(3)
在这里插入图片描述
3)一切安好。
线程一接着工作。把key(7)摘下来,放到newTable[i]的第一个,然后把e和next往下移。
在这里插入图片描述
4)环形链接出现。

e.next = newTable[i] 导致 key(3).next 指向了 key(7)

注意:此时的key(7).next 已经指向了key(3), 环形链表就这样出现了。
在这里插入图片描述
于是,当我们的线程一调用到,HashTable.get(11)时,悲剧就出现了——Infinite Loop。

JAVA HASHMAP的死循环
HashMap面试指南
Java集合之一—HashMap