面试官让我讲一下HashMap,我反手问他什么是HashMap?

HashMap实现原理

HashMap是一个用于存储Key-Value键值对的集合,每一个键值对也叫做Entry(包括Key-Value),其中Key 和 Value 允许为null。这些键值对(Entry)分散存储在一个数组当中,这个数组就是HashMap的主干。另外,HashMap数组每一个元素的初始值都是Null。

20210620235429480.png#pic_center

  值得注意的是:HashMap不能保证映射的顺序,插入后的数据顺序也不能保证一直不变(如扩容后rehash)。

  要说HashMap的原理,首先要先了解他的数据结构,

watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM5MzkwNTQ1,size_16,color_FFFFFF,t_70#pic_center

  如上图为JDK1.8版本的数据结构,其实HashMap在JDK1.7及以前是一个“链表散列”的数据结构,即数组 + 链表的结合体。JDK8优化为:数组+链表+红黑树。

  我们常把数组中的每一个节点称为一个桶。当向桶中添加一个键值对时,首先计算键值对中key的hash值(hash(key)),以此确定插入数组中的位置(即哪个桶),但是可能存在同一hash值的元素已经被放在数组同一位置了,这种现象称为碰撞,这时按照尾插法(jdk1.7及以前为头插法)的方式添加key-value到同一hash值的元素的最后面,链表就这样形成了。当链表长度超过8(TREEIFY_THRESHOLD - 阈值)时,链表就自行转为红黑树。当树节点个数小于6时,红黑树就会自动转为链表。

注意:同一hash值的元素指的是key内容一样么?不是。根据hash算法的计算方式,是将key值转为一个32位的int值(近似取值),key值不同但key值相近的很可能hash值相同,如key=“a”和key=“aa”等。

如何实现HashMap有序?

使用LinkedHashMap 或 TreeMap。

  • LinkedHashMap内部维护了一个单链表,有头尾节点,同时LinkedHashMap节点Entry内部除了继承HashMap的Node属性,还有before 和 after用于标识前置节点和后置节点。可以实现按插入的顺序或访问顺序排序。
  • TreeMap是按照Key的自然顺序或者Comprator的顺序进行排序,内部是通过红黑树来实现。TreeMap实现了SortedMap接口,它是一个key有序的Map类。要么key所属的类实现Comparable接口,或者自定义一个实现了Comparator接口的比较器,传给TreeMap用于key的比较。

put方法原理是怎么实现的?

20210620235628743.png?x-oss-process=image,size_16,color_FFFFFF,t_70#pic_center

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

put(K key, V value)可以分为三个步骤:

  1. 通过hash(Object key)方法计算key的哈希值。
  2. 通过putVal(hash(key), key, value, false, true)方法实现功能。
  3. 返回putVal方法返回的结果。

HashMap扩容机制原理

  • 数组的初始化默认长度capacity 即容量,默认16。
  • 数组扩容的loadFactor 加载因子,默认是0.75
  • 数组扩容的threshold 阈值。阈值=容量*加载因子。默认12。当元素数量超过阈值时便会触发扩容。

一般情况下,当元素数量超过阈值时便会触发扩容(调用resize()方法)。
每次扩容的容量都是之前容量的2倍。
扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。

元素迁移

JDK7的元素迁移

JDK7中,HashMap的内部数据保存的都是链表。因此逻辑相对简单:在准备好新的数组后,map会遍历数组的每个“桶”,然后遍历桶中的每个Entity,重新计算其hash值(也有可能不计算),找到新数组中的对应位置,以头插法插入新的链表。因为是头插法,因此新旧链表的元素位置会发生转置现象。元素迁移的过程中在多线程情境下有可能会触发死循环(无限进行链表反转)。
20210621003843854.png?x-oss-process=imagesize_16,color_FFFFFF,t_70#pic_center

这个过程为,先将A复制到新的hash表中,然后接着复制B到链头(A的前边:B.next=A),本来B.next=null,到此也就结束了(跟线程二一样的过程),但是,由于线程二扩容的原因,将B.next=A,所以,这里继续复制A,让A.next=B,由此,环形链表出现:B.next=A; A.next=B。

  使用头插会改变链表的上的顺序,但是如果使用尾插,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了。就是说原本是A->B,在扩容后那个链表还是A->B。


JDK8的元素迁移

  JDK1.8则因为巧妙的设计,性能有了大大的提升:由于数组的容量是以2的幂次方扩容的,那么一个Entity在扩容时,新的位置要么在原位置,要么在原长度+原位置的位置。原因如下图:

20210620235700900.png#pic_center

  数组长度变为原来的2倍,表现在二进制上就是多了一个高位参与数组下标确定。此时,一个元素通过hash转换坐标的方法计算后,恰好出现一个现象:最高位是0则坐标不变,最高位是1则坐标变为“10000+原坐标”,即“原长度+原坐标”。如下图:

20210620235713257.jpg#pic_center

  因此,在扩容时,不需要重新计算元素的hash了,只需要判断最高位是1还是0就好了。

JDK8的HashMap还有以下细节需要注意:

  • JDK8在迁移元素时是正序的,不会出现链表转置的发生。
  • 如果某个桶内的元素超过8个,则会将链表转化成红黑树,加快数据查询效率。

HashMap在JDK1.8都做了哪些优化?

watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L05vdmljZVo=,size_16,color_FFFFFF,t_70

HashMap是线程安全的吗?

不是线程安全的,在多线程环境下,

JDK1.7:会产生死循环、数据丢失、数据覆盖的问题;
JDK1.8:中会有数据覆盖的问题。
  以1.8为例,当A线程判断index位置为空后正好挂起,B线程开始往index位置写入数据时,这时A线程恢复,执行写入操作,这样A或B数据就被覆盖了。


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

 在Java中有HashTable、SynchronizedMap、ConcurrentHashMap这三种是实现线程安全的Map。

  • HashTable:是直接在操作方法上加synchronized关键字,锁住整个数组,粒度比较大;
  • SynchronizedMap:是使用Collections集合工具的内部类,通过传入Map封装出一个SynchronizedMap对象,内部定义了一个对象锁,方法内通过对象锁实现;
  • ConcurrentHashMap:使用分段锁(CAS + synchronized相结合),降低了锁粒度,大大提高并发度。

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值