Java集合之HashMap知多少

一.引言    java

 HashMap应该算是Java后端工程师面试的必问题,由于其中的知识点太多,很适合用来考察面试者的Java基础。面试

 HashMap做为使用频率最高的用于映射(键值对)处理的数据类型。随着JDK(Java Developmet Kit)版本的更新,JDK1.8对HashMap底层的实现进行了优化, 例如引入红黑树的数据结构和扩容的优化等。 结合JDK1.7和JDK1.8的区别让咱们一块儿来探讨一下吧!算法

二.简介数据库

        Hash法的概念后端

         散列法(Hashing)是一种将字符组成的字符串转换为固定长度(通常是更短长度)的数值或索引值的方法,称为散列法,也叫哈希法。因为经过更短的哈希值比用原始值进行数据库搜索更快,这种方法通常用来在数据库中创建索引并进行搜索,同时还用在各类解密算法中。数组

        HashMap 和 HashTable安全

        Hashtable 是早期Java类库提供的一个哈希表实现, 不少映射的经常使用功能与HashMap相似,不一样的是它承自Dictionary类,而且是线程安全的,任一时间只有一个线程能写Hashtable ,不支持 null 键和null值,因为同步致使的性能开销,因此已经不多被推荐使用。数据结构

        HashMap与 HashTable主要区别在于 HashMap 不是同步的,支持 null 键和null值等。一般状况下,HashMap 进行 put 或者 get 操做,能够达到常数时间的性能,因此它是绝大部分利用键值对存取场景的首选。HashMap非线程安全,即任一时刻能够有多个线程同时写HashMap,可能会致使数据的不一致。若是须要知足线程安全,能够用 Collections的synchronizedMap方法使HashMap具备线程安全的能力,或者使用ConcurrentHashMap。多线程

三.HashMap的实现原理并发

     HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,若是定位到的数组位置不含链表(当前entry的next指向null),那么查找,添加等操做很快,仅需一次寻址便可;若是定位到的数组包含链表,对于添加操做,其时间复杂度为O(n),首先遍历链表,存在即覆盖,不然新增;对于查找操做来说,仍需遍历链表,而后经过key对象的equals方法逐一比对查找。因此,性能考虑,HashMap中的链表出现越少,性能才会越好。

    

 哈希冲突

咱们都知道判断对象是否相等的时候可使用 hash值这种方式来,同时咱们还知道这种方式是存在缺点的,有可能两个对象的内容不正确,可是两个对象的hash值却同样,这是存在的.

   那么, 若是两个不一样的元素,经过哈希函数得出的实际存储地址相同怎么办 也就是说,当咱们对某个元素进行哈希运算,获得一个存储地址,而后要进行插入的时候,发现已经被其余元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞

    数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证获得的存储地址绝对不发生冲突。那么哈希冲突如何解决呢?

  咱们能够经过如下几种方式解决 : 

针对哈希表直接定址可能存在hash冲突,举一个简单的例子,例如:
    第一个键值对A进来,经过计算其key的hash获得的index=0。记作:Entry[0] = A。
    第二个键值对B,经过计算其index也等于0, HashMap会将B.next =A,Entry[0] =B,
    第三个键值对C,经过计算其index也等于0,那么C.next = B,Entry[0] = C;
这样咱们发现index=0的地方事实上存取了A,B,C三个键值对,它们经过next这个属性连接在一块儿。 对于不一样的元素,可能计算出了相同的函数值,这样就产生了hash 冲突,那要解决冲突,又有哪些方法呢?具体以下:

a. 链地址法:将哈希表的每一个单元做为链表的头结点,全部哈希地址为 i 的元素构成一个同义词链表。即发生冲突时就把该关键字链在以该单元为头结点的链表的尾部。

b. 开放定址法:即发生冲突时,去寻找下一个空的哈希地址。只要哈希表足够大,总能找到空的哈希地址。

c. 再哈希法:即发生冲突时,由其余的函数再计算一次哈希值。

d. 创建公共溢出区:将哈希表分为基本表和溢出表,发生冲突时,将冲突的元素放入溢出表。

而咱们的HashMap采用的是链地址法,即上面提到 数组 + 链表的方式. 当两个对象的哈希值相同时,它们的哈希桶位置相同,碰撞就会发生。此时,能够将 put 进来的 K- V 对象插入到链表的尾部。对于储存在同一个哈希桶位置的链表对象,可经过键对象的equals()方法用来找到键值对。

四.HashMap的内部数据结构

        HashMap内部维护的数据结构是数组+链表,每一个键值对都存储在HashMap的静态内部类Entry中,结构如图 :

五 . HashMap插入数据原理图

  1. 判断数组是否为空,为空进行初始化;
  2. 不为空,计算 k 的 hash 值,经过 (n-1) & hash 计算应当存放在数组中的下标 index;
  3. 查看 table[index] 是否存在数据,没有数据就构造一个Node节点存放在 table[index] 中;
  4. 存在数据,说明发生了hash冲突(存在二个节点key的hash值同样), 继续判断key是否相等,相等,用新的value替换原数据(onlyIfAbsent为false);
  5. 若是不相等,判断当前节点类型是否是树型节点,若是是树型节点,创造树型节点插入红黑树中;(若是当前节点是树型节点证实当前已是红黑树了)
  6. 若是不是树型节点,建立普通Node加入链表中;判断链表长度是否大于 8而且数组长度大于64, 大于的话链表转换为红黑树;
  7. 插入完成以后判断当前节点数是否大于阈值,若是大于开始扩容为原数组的二倍。

六.HashMap 是否线程安全?如何解决线程不安全的问题

    HashMap是否线程安全?    

        不是,在多线程环境下,1.7 会产生死循环、数据丢失、数据覆盖的问题,1.8 中会有数据覆盖的问题,以1.8为例,当A线程判断index位置为空后正好挂起,B线程开始往index位置的写入节点数据,这时A线程恢复现场,执行赋值操做,就把A线程的数据给覆盖了;还有++size这个地方也会形成多线程同时扩容等问题。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
  Node<K,V>[] tab; Node<K,V> p; int n, i;
  if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;
  if ((p = tab[i = (n - 1) & hash]) == null)  //多线程执行到这里
    tab[i] = newNode(hash, key, value, null);
  else {
    Node<K,V> e; K k;
    if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k))))
      e = p;
    else if (p instanceof TreeNode) // 这里很重要
      e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    else {
      for (int binCount = 0; ; ++binCount) {
        if ((e = p.next) == null) {
          p.next = newNode(hash, key, value, null);
          if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
            treeifyBin(tab, hash);
          break;
        }
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
          break;
        p = e;
      }
    }
    if (e != null) { // existing mapping for key
      V oldValue = e.value;
      if (!onlyIfAbsent || oldValue == null)
        e.value = value;
      afterNodeAccess(e);
      return oldValue;
    }
  }
  ++modCount;
  if (++size > threshold) // 多个线程走到这,可能重复resize()
    resize();
  afterNodeInsertion(evict);
  return null;
}

如何解决线程不安全的问题?

Java中有HashTable、Collections.synchronizedMap、以及ConcurrentHashMap能够实现线程安全的Map。

HashTable是直接在操做方法上加synchronized关键字,锁住整个数组,粒度比较大;

Collections.synchronizedMap是使用Collections集合工具的内部类,经过传入Map封装出一个SynchronizedMap对象,内部定义了一个对象锁,方法内经过对象锁实现;

ConcurrentHashMap使用分段锁,下降了锁粒度,让并发度大大提升。

 

七.HashMap 在 JDk 1.8 和 JDK 1.7 中有什么区别?

 1.发生hash冲突时 

    JDK 1.7  :

             发生hash冲突时,新元素插入到链表头中,即新元素老是添加到数组中,就元素移动到链表中。

    JDK 1.8 :

             发生hash冲突后,会优先判断该节点的数据结构式是红黑树仍是链表,若是是红黑树,则在红黑树中插入数据;若是是链表,则将数据插入到链表的尾部并判断链表长度是否大于8,若是大于8要转成红黑树。

2.新增节点时

        JDK 1.7 :

              使用了 头插法

        JDK 18. :

               使用了 尾插法
3.扩容时

     JDK 1.7 :

          在扩容resize()过程当中,采用单链表的头插入方式,在将旧数组上的数据 转移到 新数组上时,转移操做 = 按旧链表的正序遍历链表、在新链表的头部依次插入,即在转移数据、扩容后,容易出现链表逆序的状况 。 多线程下resize()容易出现死循环。此时若(多线程)并发执行 put()操做,一旦出现扩容状况,则 容易出现 环形链表,从而在获取数据、遍历链表时 造成死循环(Infinite Loop),即 死锁的状态 。

    JDK 1.8 :

        因为 JDK 1.8 转移数据操做 = 按旧链表的正序遍历链表、在新链表的尾部依次插入,因此不会出现链表 逆序、倒置的状况,故不容易出现环形链表的状况 ,但jdk1.8还是线程不安全的,由于没有加同步锁保护。

建议 : 

    1.使用的时候设置初始化值,避免屡次扩容的性能消耗

    2.使用自定义对象做为key时,须要重写hashCode()和equals()方法

    3.多线程环境尽可能使用ConcurrentHashMap来代替HashMap,HashTable也能够,可是用的少

这就是小喵今天的分享了

知识有点乱,还请小伙伴们到时候缕缕思路哦!

(^_^)~喵~!!

相关文章
相关标签/搜索