HashMap及一些相关问题汇总

一、什么是HashMap

HashMap,中文名哈希映射,HashMap是一个用于存储Key-Value键值对的集合,每一个键值对也叫做Entry。这些个键值对(Entry)分散存储在一个数组当中,这个数组就是HashMap的主干。HashMap数组每一个元素的初始值都是Null。HashMap是基于哈希表的 Map 接口的实现。

二、Map所具有的特点

1.没有重复的 key(一方面,key用set保存,所以key必须是唯一,无序的;另一方面,map的取值基本上是通过key来获取value,如果有两个相同的key,计算机将不知道到底获取哪个对应值;这时候有可能会问,那为什么我编程时候可以用put()方法传入两个key值相同的键值对?那是因为源码中,传入key值相同的键值对,将作为覆盖处理)

2.每个 key 只能对应一个 value, 多个 key 可以对应一个 value

3.key,value 都可以是任何引用类型(包括 null)的数据(只能是引用类型)

4.Map 取代了古老的 Dictionary 抽象类

下图为Map的存储结构
HashMap及一些相关问题汇总

三、HashMap的底层原理及存储结构

首先有一个每个元素都是链表(可能表述不准确)的数组,当添加一个元素(key-value)时,就首先计算元素key的hash值,以此确定插入数组中的位置,但是可能存在同一hash值的元素已经被放在数组同一位置了,这时就添加到同一hash值的元素的后面,他们在数组的同一位置,但是形成了链表,同一各链表上的Hash值是相同的,所以说数组存放的是链表。而当链表长度太长时,链表就转换为红黑树,这样大大提高了查找的效率。
当链表数组的容量超过初始容量的0.75时,再散列将链表数组扩大2倍,把原链表数组的搬移到新的数组中

HashMap的存储结构(JDK1.8)
HashMap及一些相关问题汇总

四、HashMap在 jdk1.7 和 jdk1.8 中的区别

HashMap及一些相关问题汇总
其中,我们需要着重注意HashMap存储方式和其存放数据规则的变化。

在jdk1.7中我们通常采用 数组+链表 的方式来存储数据,而jdk1.8中HashMap的存储结构变成了 数组+链表+红黑树 的组合。相对于JDK1.7,HashMap处理hash冲突时,会首先存放在链表中去,但是一旦链表中的数据较多(即>8个)之后,就会转用红黑树来进行存储,优化存储速度,O(lgn)。如果是链表。一定是O(n)。

HashMap的存储结构(jdk1.7)
HashMap及一些相关问题汇总

同时,jdk1.8相较于jdk1.7,扰动函数也变的更加简化

//jdk1.7
final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
 
        h ^= k.hashCode();
 
        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
}

//jdk1.8
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

五、JDK1.7中HashMap在多线程环境中的问题

HashMap及一些相关问题汇总

//HashMap的rehash源代码
void transfer(Entry[] newTable, boolean rehash) {
      int newCapacity = newTable.length;
     //for循环中的代码,逐个遍历链表,重新计算索引位置,将老数组数据复制到新数组中去(数组不存储实际数据,所以仅仅是拷贝引用而已)和 arraylist 或者 linkedlist 中的clone方法是一样的 都是浅拷贝关系
      foreach (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);
          //将当前entry的next链指向新的索引位置,newTable[i]有可能为空,有可能也是个entry链,如果是entry链,直接在链表头部插入。
          //第一次时 newTable[i] = null

              e.next = newTable[i];
              newTable[i] = e;
              e = next;
          }
      }
  }
//HashMap的transfer源代码
void transfer(Entry[] newTable, boolean rehash) {
   //新table的容量
   int newCapacity = newTable.length;
   //遍历原table
   for (Entry<K,V> e : table) {
       while(null != e) {
           //保存下一次循环的 Entry<K,V>
           Entry<K,V> next = e.next;
           if (rehash) {
               //通过e的key值计算e的hash值
               e.hash = null == e.key ? 0 : hash(e.key);
           }
           //得到e在新table中的插入位置
           int i = indexFor(e.hash, newCapacity);
           //采用链头插入法将e插入i位置,最后得到的链表相对于原table正好是头尾相反的
           e.next = newTable[i];
           newTable[i] = e;
           //下一次循环
           e = next;
       }
   }
}

起因主要是hashmap在put数据时,超过预设长度则会自动扩容,即resize方法,而引起死锁的核心逻辑为resize中的transfer方法。
JDK1.8之前,为了提高rehash的速度,冲突链表是使用头插法,因为头插法是操作速度最快的,找到数组位置就直接找到插入位置了,头插法在多线程下回引起死循环
JDK1.8之后开始加入红黑树,当链表长度大于8时链表就会转换成红黑树,这样就大大提高了在冲突链表查找的速度,同时因为链表的长度不可能大于8,链表在rehash的消耗就小很多,所以JDK1.8使用尾插法也避免了死循环问题

上一篇:echarts 5.0 仪表盘gauge


下一篇:SMM整合配置模板