第一部分,面试会遇到的问题
- HashMap在JDK7 到 JDK8的变化
- JDK1.7中,HashMap是基于单链表 + 数组的模式实现的;
- JDK1.8中,HashMap是基于链表+数组+红黑树的数据结构实现的;
- JDK1.7中插入的方式是头插法,1.8中使用的是尾插法:
- 1.7中基于单链表的实现,使用头插法会造成出现逆序、环形链表死循环的问题。当HashMap数据量达到总长度【Capacity】的75%(默认,具体值在HashMap类中的【LoadFactor】,即【负载因子】 , 初始化值为0.75f)时,会对HashMap进行扩容,原则上是将其长度扩大为原本的两倍,这个过程称为【resize】。resize的过程如下:
- 创建一个新的Entry数组,长度是原HashMap的两倍(二进制+1位);
- ReHash:因为长度扩大后,hash的规则也会随之改变【hash公式:index = HashCode(Key) & (Length - 1)】,所以要对HashMap进行重新Hash计算;
- 在多线程情况下,如果多个线程同时对一个HashMap进行添加【put】操作,并同时触发扩容,会导致在Rehash的某一时刻链表形成闭环(死循环),这个过程如果想详细了解,可以参考JDK7中HashMap的死循环 小言老师的文章,总结的非常到位。大多时候讲到这里就不需要继续深究原因了;
- 一般在JDK8之前想要保证线程安全,可以使用ConcurrentHashMap保证线程安全。
- JDK8号称可以避免死循环和倒序的问题,但是在实际应用中,有大佬发现在高并发场景下,红黑树实现时可能会出现多个树节点的Parent互相引用,仍会造成死循环的现象。【参考:JDK8中HashMap的死循环】
- 真的到了多线程高并发的场景,老老实实用ConcurrentHashMap吧。
- 1.7中基于单链表的实现,使用头插法会造成出现逆序、环形链表死循环的问题。当HashMap数据量达到总长度【Capacity】的75%(默认,具体值在HashMap类中的【LoadFactor】,即【负载因子】 , 初始化值为0.75f)时,会对HashMap进行扩容,原则上是将其长度扩大为原本的两倍,这个过程称为【resize】。resize的过程如下:
- JDK7-8中,HashMap都不是线程安全的,主要是因为put操作未加锁,在多线程的场景下会造成线程的不同步操作导致数据有效性、及时性问题。
- HashMap的初始大小:HashMap的初始大小是16,主要是因为:
- HashMap的大小Capacity是2的n次幂;
- 2/4/8虽然可行,但是过于容易触发HashMap的扩容resize,这个过程可能会造成性能损失;
- 只要输入的HashCode是均匀的,hash算法也会相对均匀,初始大小16也会使该分部均匀。
- 为什么用ConcurrentHashMap而不用HashTable来保证线程安全:
- HashTable只是用synchronized实现了同步锁;
- 这样实现的同步锁并发度不高;
- ConcurrentHashMap使用了锁分段(减小锁的范围),CAS(Compare And Swap 比较并交换,乐观锁,减少上下文开销,无阻塞),在一定程度上保证了线程安全同时保证较高的并发度。
- JDK8中什么时候用链表,什么时候用红黑树?原因是什么?
- 长度达到8时由链表转换为红黑树,长度小于6时由红黑树转换为链表;
- 执行get操作,链表的时间复杂度是n/2 ,红黑树是 log n (log以2为底,算对数) ;
- 当长度为8时,链表:8/2 = 4 红黑树 : log 8 = 3 ,此时红黑树时间效率高于链表;
- 当长度为4时,链表:4/2 = 2 红黑树 : log 4 = 2 ,此时时间效率相同,但是空间复杂度,链表要优于红黑树;
- 在4-8的区间内,通常认为红黑树效率略高于链表,但是空间复杂度远高于链表,所以还是优先使用链表结构;
- 实际上讲,从长度大于4时开始,红黑树的时间效率已高于链表结构,但是考虑到较高的空间复杂度,选择了链表结构,此中涉及的是时间、空间的取舍;
- 6-8的区间内,不会改变原本的数据结构,因为改变数据结构的操作本身就有一定开销,所以不进行数据结构的改变。