面试专题-刨根问底篇

1.HashMap底层原理

1.1.HashMap概述

HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

1.2.HashMap的数据结构

HashMap实际上是一个“数组+链表+红黑树”的数据结构

1.3.HashMap的存取实现:

1.8之前的

当我们往HashMap中put元素的时候,先根据key的hashCode重新计算hash值,根据hash值得到这个元素在数组中的位置(即下标),如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。

1.8

put():

1. 根据key计算得到key.hash = (h = k.hashCode()) ^ (h >>> 16);
2. 根据key.hash计算得到桶数组的索引index = key.hash & (table.length - 1),这样就找到该key的存放位置了:
① 如果该位置没有数据,用该数据新生成一个节点保存新数据,返回null;
② 如果该位置有数据是一个红黑树,那么执行相应的插入 / 更新操作
③ 如果该位置有数据是一个链表,分两种情况一是该链表没有这个节点,另一个是该链表上有这个节点,注意这里判断的依据是key.hash是否一样: 如果该链表没有这个节点,那么采用尾插法新增节点保存新数据,返回null; 如果该链表已经有这个节点了,那么找到該节点并更新新数据,返回老数据。 注意: HashMap的put会返回key的上一次保存的数据。

get():

计算需获取数据的hash值(计算过程跟put一样),计算存放在数组table中的位置(计算过程跟put一样),然后依次在数组,红黑树,链表中查找(通过equals()判断),最后再判断获取的数据是否为空,若为空返回null否则返回该数据

1.4.树化与还原

  • 哈希表的最小树形化容量
  • 当哈希表中的容量大于这个值时(64),表中的桶才能进行树形化
  • 否则桶内元素太多时会扩容,而不是树形化
  • 为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
  • 一个桶的树化阈值
  • 当桶中元素个数超过这个值时(8),需要使用红黑树节点替换链表节点
  • 这个值必须为 8,要不然频繁转换效率也不高
  • 一个树的链表还原阈值
  • 当扩容时,桶中元素个数小于这个值(6),就会把树形的桶元素 还原(切分)为链表结构
  • 这个值应该比上面那个小,至少为 6,避免频繁转换

条件1. 如果当前桶数组为null或者桶数组的长度 < MIN_TREEIFY_CAPACITY(64),则进行扩容处理.
条件2. 当不满足条件1的时候则将桶中链表内的元素转换成红黑树!!!

1.5.扩容机制的实现

1. 扩容(resize)就是重新计算容量。当向HashMap对象里不停的添加元素,而HashMap对象内部的桶数组无法装载更多的元素时,HashMap对象就需要扩大桶数组的长度,以便能装入更多的元素。

2. capacity 就是数组的长度/大小,loadFactor 是这个数组填满程度的最大比比例。 

3. size表示当前HashMap中已经储存的Node<key,value>的数量,包括桶数组和链表 / 红黑树中的的Node<key,value>。

4. threshold表示扩容的临界值,如果size大于这个值,则必需调用resize()方法进行扩容。 

5. 在jdk1.7及以前,threshold = capacity * loadFactor,其中 capacity 为桶数组的长度。 这里需要说明一点,默认负载因子0.75是是对空间和时间(纵向横向)效率的一个平衡选择,建议大家不要修改。 jdk1.8对threshold值进行了改进,通过一系列位移操作算法最后得到一个power of two size的值

1.6.什么时候扩容

当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值—即当前数组的长度乘以加载因子的值的时候,就要自动扩容啦。

扩容必须满足两个条件:

1、 存放新值的时候   当前已有元素的个数  (size) 必须大于等于阈值
2、 存放新值的时候当前存放数据发生hash碰撞(当前key计算的hash值换算出来的数组下标位置已经存在值)
//如果计算的哈希位置有值(及hash冲突),且key值一样,则覆盖原值value,并返回原值value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
   V oldValue = e.value;
    e.value = value;
   e.recordAccess(this);
   return oldValue;
}

resize()方法: 该函数有2种使用情况

1.初始化哈希表 
2.当前数组容量过小,需扩容

1.6.扩容过程

插入键值对时发现容量不足,调用resize()方法方法,

1.首先进行异常情况的判断,如是否需要初始化,二是若当前容量》最大值则不扩容,

2.然后根据新容量(是就容量的2倍)新建数组,将旧数组上的数据(键值对)转移到新的数组中,这里包括:(遍历旧数组的每个元素,重新计算每个数据在数组中的存放位置(原位置或者原位置+旧容量),将旧数组上的每个数据逐个转移到新数组中,这里采用的是尾插法。)

3.新数组table引用到HashMap的table属性上

4.最后重新设置扩容阙值,此时哈希表table=扩容后(2倍)&转移了旧数据的新table

2.HashSet底层原理

2.1.HashSet概述

HashSet实现Set接口,由哈希表(实际上是一个HashMap实例)支持。它不保证set 的迭代顺序;特别是它不保证该顺序恒久不变。此类允许使用null元素。

2.2.HashSet的实现

对于HashSet而言,它是基于HashMap实现的,HashSet底层使用HashMap来保存所有元素,因此HashSet 的实现比较简单,相关HashSet的操作,基本上都是直接调用底层HashMap的相关方法来完成, (实际底层会初始化一个空的HashMap,并使用默认初始容量为16和加载因子0.75。)

对于HashSet中保存的对象,请注意正确重写其equals和hashCode方法,以保证放入的对象的唯一性。

2.3.插入

当有新值加入时,底层的HashMap会判断Key值是否存在,如果不存在,则插入新值,同时这个插入的细节会依照HashMap插入细节;如果存在就不插入

3.ConcurrentHashMap的工作原理

变化:

ConcurrentHashMap的JDK8与JDK7版本的并发实现相比,最大的区别在于JDK8的锁粒度更细,理想情况下talbe数组元素的大小就是其支持并发的最大个数

实现

改进一:取消segments字段,直接采用transient volatile HashEntry<K,V>[] table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率。

数据结构

改进二:将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构。对于hash表来说,最核心的能力在于将key hash之后能均匀的分布在数组中。

概念:

JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本。
发布了274 篇原创文章 · 获赞 80 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/BruceLiu_code/article/details/103258578