关于hashmap的知识点和面试考点

HashMap是java开发中经常会被问到的知识点,下面就我搜集到的一些问题进行讲解。

最常见的问题一般是以下方面的问题:

1,它的底层数据结构是什么?

2,在java7和java8里的区别是什么?

3,线程是否安全?为什么说它不安全?

4,实际使用中,你是如何解决线程不安全问题的?

5,初始大小是什么?为什么都是2的mi?扩容机制是什么?扩容可能会出现哪些问题?

6,为甚用红黑树?为什么是8个转化为红黑树,不是9,10个?

7,是怎么减少hash碰撞问题的?

8,hash的计算规则是什么?

9,和hashtable之类的区别是什么

下面开始从头到尾的讲,上述问题的答案也会穿插在里面。

首先在java8之前,比如java7,hashmap的底层结构是数组+链表的结构,在java8,底层变成了数组+链表+红黑树的结构。而数组里存储着key-value,在java7中叫做entry,而在Java8中就叫做node。

在我们创建hashmap的时候,java其实是没有为数组分配空间的,而是在执行put操作的时候才真正构建数组,一般最开始是16。随着不断的插入,到达一定数量之后就会进行扩容,也就是resize。那什么时候继续扩容呢?hashmap有两个重要的参数,一个是capacity,是hashmap当前的长度,另一个loadfactor是负载因子,默认值是0.75f。意思是当存放超过总容量的0.75,就会自动扩容,并且扩容一般是现在容量的2倍,并且总容量一定是2的幂(原因一会会说)。而且扩容并不是在原来的基础上增加一倍,而是重新开辟一个2倍的数组,然后讲现有的数组重新rehash,存储在新的数组里。所以说扩容实际上是比较消耗资源的事情。并且rehash的过程也会出现问题(线程不安全的地方之一)。

刚刚为什么说rehash有可能出现问题呢?首先还要从hashmap插入数据的方式说起。我们刚刚提到结构是数组+链表,java8还有红黑树,那么什么时候用到链表呢,那就又要牵扯到hashcode()了,我们知道,不同的key的hashcode可能是不同的,但是也会存在key不同,但是他们的hashcode()是相同的情况。比如我在进行put操作的时候,存入(您好,上海),那么就要对关键字您好进行hashcode(),然后找到它的映射地址,然后存入相应的数组,比如您好的映射地址是12,问题来了,之前在12上已经存入了数据了,那怎么办呢,hashmap解决hash冲突的方法就是在当前node上加一个链表。此时的您好-上海和之前的key-value就构成了链表。如果后续还有hashcode相同的情况,就继续加入。

问题又来了,怎么加入呢,在Java7中使用的是头插法,意思是新加入的那个做链表的头指针,也就是占了node的位置,在Java8中采取的是尾插法,就是说后来的自动到链表的尾端。刚刚说的扩容问题就出现在java7的头插法上,我们前面提到,扩容后,我们会对原来hashmap上的所有数组重新rehash,在新的hashmap上。我们来看,比如在原结构上存在这么一条链表:A-->B-->C-->D。我们开始多线程进行rehash,我们将A分配到一个地方去,开始分配B,巧了,BDE hashcode()还是和A一样,那么采用头插法替代A的位置,指针指向A.又巧了,C,D被分配到别的地方去了,那现在B指向A,而A还是指向B的,那就造成了循环了。当我们调用它的时候,就会出现死循环了。这里要注意的是这个不是大概率事件,一般扩容之后,根据hashcode()算法,很少会一样,而且上述问题是多线程操作的一种极端情况。而在java8中,采用尾插法,扩容的时候还是会保持链表的顺序,一般不会出现循环链表的问题。

看到这里的小伙伴又说了,那么java是不是就没有线程不安全的问题了呢,肯定不是啊,在put的时候有时候也会出现问题,比如有两个线程A和B,首先A希望插入一个key -value对到hashmap中,首先计算记录所要落到的hash桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程A的时间片用完了,而此时线程B被调度得以执行,和线程A一样执行,只不过线程B成功将记录插到了桶里面,假设线程A插入的记录计算出来的hash桶索引和线程B要插入的记录计算出来的hash桶索引是一样的,那么当线程B成功插入之后,线程A再次被调度运行时,它依然持有过期的链表头但是它对此一无所知,以至于它认为它应该这样做如此一来就覆盖了线程B插入的记录,这样线程B插入的记录就凭空消失了,造成了数据不一致的行为。

我们继续主线讲解,下面就到了红黑树,为什么要引入红黑树呢,数组+链表挺香的啊。这就是一个时间复杂度的问题了。红黑树的引入将原本O(n)的时间复杂度降低到了O(logn)。java8的hashmap源码上意识是链表长度超过8个,超过的部分就会自动转化成红黑树结构,长度减到6个的时候就自动转化为链表结构。刚刚有个问题为什么不是9个,10个。这个就是概率问题了。hashCode离散性很好的时候,树型bin用到的概率非常小,因为数据均匀分布在每个bin中,几乎不会有bin中链表长度会达到阈值。但是在随机hashCode下,离散性可能会变差,然而JDK又不能阻止用户实现这种不好的hash算法,因此就可能导致不均匀的数据分布。不过理想情况下随机hashCode算法下所有bin中节点的分布频率会遵循泊松分布,我们可以看到,一个bin中链表长度达到8个元素的概率为0.00000006,几乎是不可能事件。所以,之所以选择8,不是拍拍屁股决定的,而是根据概率统计决定的。那又有人说了,那为啥不直接用红黑树呢。引入红黑树就是为了查找数据快,解决链表查询深度的问题,但是红黑树需要不断的左旋和右旋来保持平衡,比较耗费的资源,所以简单来说还是时间和空间的平衡。

此处附上一张图(我找不到原出处了,如果有朋友知道,请留言,我附上链接,指明出处)

然后我们接下来将put和get,这个又会涉及到刚刚提高的hashcode()和hash函数、还有equals。调用HashMap的put方法,首先调用hashcode方法,计算出一个hashcode,然后通过hash函数去找到映射地址。进行存储,如果此地址已经有值了,那么就插到链表的尾部。(这也是解决hash冲突的方式)。调用get方法时,首先还是进行hashcode,然后通过hash函数去查找相关的key,当有冲突时,再调用equals方法,找到链表上相应的值。

既然提高了hash函数,那么就详细讲讲,记忆上面说的为啥长度是2的幂的问题。为什么呢?当然是为了得到一个均匀分布的hash.

上面我们说过当put或者get一个key的时候,首先要计算它的hashcode,然后再根据hash函数得到位置。那么大家有没有想过hashcode怎么计算,我们先不管hash的计算规则是什么。我们知道肯定要尽量让不同的key的hashcode尽可能的离散和均匀,这样可以尽可能的避免hash冲突,在源码中式继承并重写了hashcode(又是一个知识点),最后它的映射地址计算方式是hashcode(key)&(length-1).

这就很明了了。2的幂次减一的二进制表示全部都是1,所有hashcode和它进行与运算(之所以不用算术运算是因为位运算速度快,又是一个点),就可以尽可能的保留有效位,这样就提高映射地址的离散。因此,为了减少hash冲突,还是要采用合适的equals和hashcode方法。

再来讲一下hash方法的计算规则,它又是如何减少hash冲突的呢?

 

从源码可以看出它是采用高16位和低16位异或的方式,来进一步减少冲突。

下面继续,刚刚说到,既然hashmap是线程不安全的呢,那么平时用什么代替呢?当然是hashtable和concurrenthashmap了。

先说hashtable,简单来说,直接在⽅法上锁,这就导致了并发度很低,最多同时允许⼀个线程访问。在实际开发中使用还是比较少的。一般情况下使用concurrenthashmap。

下面简单说说它,java1.7中,concurrenthashmap的数据结构为 Segment + HashEntry,采用的是锁分段技术,那么什么是锁分段技术呢。首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。用一个Segment数组维护所有的键值对,一个Segment对象的数据结构相当于一个HashMap,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。从而可以有效的提高并发访问效率。在java1.8中,它摒弃了Segment(锁段)的概念,而是启用了一种全新的方式实现,利用CAS算法。它沿用了与它同时期的HashMap版本的思想,底层是“数组”+链表+红黑树,接采用transient volatile Node<K,V>[] table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率。

以上就是绝大部分面试会问道的问题了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值