自己总结的一点hashMap面试相关知识
HR:首先介绍一下HashMap底层数据结构吧
答:因为现在大部分企业都是采用JDK1.8,底层是由“数组+链表+红黑树组成”,如下图。而在 JDK 1.8 之前是由“数组+链表”组成。
HR:为什么要改成“数组+链表+红黑树”?
主要是为了提升在 hash 冲突严重时(链表过长)的查找性能,使用链表的查找性能是 O(n),而使用红黑树是 O(logn)。
延伸1:什么是hash冲突
由于哈希算法被计算的数据是无限的,而计算后的结果范围有限,因此总会存在不同的数据经过计算后得到的值相同,这就是哈希冲突。(两个不同的数据计算后的结果一样)
延伸2:解决hash冲突方法
1、开放地址法(再散列法)
发生哈希冲突后,按照某一次序找到下一个空闲的单元,把冲突的元素放入。
- 线性探查法
从发生冲突的单元开始探查,依次查看下一个单元是否为空,如果到了最后一个单元还是空,那么再从表首依次判断。如此执行直到碰到了空闲的单元或者已经探查完所有单元。(简而言之就是每次都是表头到表尾一个一个单元的循环探测空闲单元)
- 平方探查法
从发生冲突的单元加上12,22,32,…,n2,直到遇到空闲的单元
- 双散列函数探查法
定义两个散列函数,分别为s1和s2,s1的算法和前面一致,s2取一个1~m-1之间并和m互为素数的数。s2作为步长。
2、链地址法(拉链法)
这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。
3、再哈希法
这种方法是同时构造多个不同的哈希函数:
Hi=RH1(key)i=1,2,…,k
当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。
4、创建公共溢出区
这种方法的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表
延伸3:HashMap解决hash冲突方法
hashmap出现了Hash冲突的时候采用第二种办法:链地址法。
HR:那在什么时候用链表?什么时候用红黑树?
对于插入,默认情况下是使用链表节点。当同一个索引位置的节点在**新增后达到9个(阈值8)
**:如果此时数组长度大于等于 64,则会触发链表节点转红黑树节点(treeifyBin);而如果数组长度小于64,则不会触发链表转红黑树,而是会进行扩容,因为此时的数据量还比较小。
对于移除,当同一个索引位置的节点在移除后达到 6 个,并且该索引位置的节点为红黑树节点,会触发红黑树节点转链表节点(untreeify)。
HR:为什么链表转红黑树的阈值是8?
我们平时在进行方案设计时,必须考虑的两个很重要的因素是:时间和空间。对于 HashMap 也是同样的道理,简单来说,阈值为8是在时间和空间上权衡的结果。
红黑树节点大小约为链表节点的2倍,在节点太少时,红黑树的查找性能优势并不明显,付出2倍空间的代价不值得。
理想情况下,使用随机的哈希码,节点分布在 hash 桶中的频率遵循泊松分布,按照泊松分布的公式计算,链表中节点个数为8时的概率为 0.00000006,这个概率足够低了,并且到8个节点时,红黑树的性能优势也会开始展现出来,因此8是一个较合理的数字。
HR:HashMap的初始容量与扩容
HashMap的初始容量为16,加载因子默认是0.75,HashMap扩容时是当前容量翻倍即:capacity*25
HR:除了 HashMap,还用过哪些 Map,在使用时怎么选择?
HR: HashMap如何实现线程安全?
(1).通过collections.synchroizedMap()的接口,返回的是一个Map,这个map就是安全的,这个需要面向接口编程,返回的是一个Map
(2).重写HashMap,通过java.util.Concurrent.ConcurrentHashMap(),把hashmap里面的方法进行拆分
从4个方面来理解:
1.实现机制:
1.使用collections.synchronizedMap方法封装所有不安全的方法,包括hashcode,toString(),通过synchronized来实现互斥。
2.重写hashMap方法,采用新的锁是:Nonfairsync,将hashMap拆分成各个独立的块,减少锁之间的冲突
2.锁机制不同:
方法一:使用的是synchronized,是一种悲观的,在进入之前必须获取锁,确保当前是独享状态,后面的都是在等待!
方法二:乐观锁;只有在需要修改对象的同时,比较和之前的值是否发生变化,如果被人修改了,就返回失败,而锁的实现是使用NonfairSync,这个特性是来保证原子性和互斥性,无法在jdk这个级别得到理解,jdk调用jni方法,jni方法通过调用cas指令确保原子性和互斥性!
乐观锁的实现原理:当多个线程恰好操作到concurrentHashMap同一个segment上面是,只会有一个线程得到运作,其他的都被LockSupport拦截,稍后执行以后,会自动挑选一个线程来执行LockSupport
3.释放锁:
1.方法一,锁住的是对象,当获取执行权的时候,其他线程进入阻塞状态,等待唤醒
2.方法二:state ,如果为0,就会得到锁,如果已经得到了还可以再次得到!state+1;
释放锁两个都一样,都是逆向的,两种方法都是自己通过队列获取到锁!
4.优缺点:
1.方法简单,但是因为线程安全,造成大量线程阻塞问题,影响性能
2.互斥代码比较少,性能高,但是因为hashMap被分解了很多块,所以发生碰撞的机会大大降低了!!代码实现稍稍复杂些.
拓展1:Hash算法
这类算法接受任意长度的二进制输入值,对输入值做换算(切碎),最终给出固定长度的二进制输出值;
作用:
-
信息安全领域:
Hash算法 可用作加密算法。
如文件校验:通过对文件摘要,可以得到文件的“数字指纹”,你下载的任何副本的“数字指纹”只要和官方给出的“数字指纹”一致,那么就可以知道这是未经篡改的。例如著名的MD5 ; -
数据结构领域:
Hash算法 通常还可用作快速查找。
这是今天我想说的部分。根据Hash函数 我们可以实现一种叫做***哈希表(Hash Table)***的数据结构。这种结构可以实现对数据进行快速的存取。
拓展2:哈希表与哈希函数
- 哈希函数
-
灵活
哈希函数是一个映像,因此哈希函数的设定很灵活,只要使得任何关键字由此所得的哈希函数值都落在表长允许的范围之内即可。 -
冲突
对不同的关键字可能得到同一哈希地址,即 ,这种现象称为冲突(collision);冲突只能尽量地少,而不能完全避免。因为,哈希函数是从关键字集合到地址集合的映像。而通常关键字集合比较大,它的元素包括所有可能的关键字,而地址集合的元素仅为哈希表中的地址值。因此,在实现哈希表这种数据结构的时候不仅要设定一个“好”的哈希函数,而且要设定一种处理冲突的方法。
- 哈希表
量地少,而不能完全避免。因为,哈希函数是从关键字集合到地址集合的映像。而通常关键字集合比较大,它的元素包括所有可能的关键字,而地址集合的元素仅为哈希表中的地址值。因此,在实现哈希表这种数据结构的时候不仅要设定一个“好”的哈希函数,而且要设定一种处理冲突的方法。
-
哈希表
根据设定的Hash函数 - 和处理冲突的方法,将一组关键字映象到一个有限的连续的地址集(区间)上,并以关键字在地址集中的象作为记录在表中的存储位置,这样的表便称为Hash表 ;