上一篇文章分析了hashmap的主要方法,了解了主要方法接下来好解答平时面试的问题了。
加载因子为什么是 0.75?
那加载因子为什么是 0.75 而不是 0.5 或者 1.0 呢?
首先如果加载因子比较大,那么扩容发生的频率就比较低,但是他浪费的空间比较小,不过发生hash冲突的几率就比较大,比如加载因子是1的时候,如果hashmap长度为128,那么可能hashmap的实际存储元素数量在64至128之间的时间段比较多,而这个时间段发生hash冲突就比较大,造成数组中其中一条链表较长,就会影响性能。
而当加载因子值比较小的时候,扩容的频率就会变高,因此会占用更多的空间,但是元素的存储就比较稀疏,发生哈希冲突的可能性就比较小,因此操作性能会比较高,比如设置成0.5,同样128长度的hashmap,当数量达到65的时候就会触发hashmap的扩容,扩容后长度为256,256里面只存储了65个似乎有点浪费了。
所以综合了以上情况就取了一个 0.5 到 1.0 的平均数 0.75 作为加载因子。
当有哈希冲突时,HashMap 是如何查找并确认元素的?
通过昨天分析get方法应该已经比较清楚了,如果哈希冲突,还是找到table[index]的第一个Node,然后一个一个去比对链表中的key,key一致则找到,引用put流程图其中一部分如下图:
JDK 1.8 HashMap 扩容时做了哪些优化?
在 JDK 1.7 中 HashMap 是以数组加链表的形式组成的,JDK 1.8 之后新增了红黑树的组成结构,当链表大于 8 并且容量大于 64 时,链表结构会转换成红黑树结构,即使在hashcode 完全相同极端情况下,由于红黑树的特点,查找某个特定元素,也只需要O(log n)的开销,而如果查找链表,时间复杂度会退化到 O(n)。
HashMap 是线程安全的吗,为什么不是线程安全的?
通过例子来讲解这个不安全的过程,引用上一篇文章的例子,首先一个长度为8的map<Integer,V>,map已经存了key为1、9的两个值,他们都会在map的table[1]上。
1、首先线程1put一个key=17的值,当程序运行到判断key=9的下一个节点为null准备把key=17的值设置为它的下一个节点时,线程让出资源。
2、此时执行线程2,对mapput一个key=25的数据,这个数据正常作为key=9的next正常插入。
3、线程1再次拿到资源继续执行,设置key=9的next为key=17,这样线程2设置的key=25就被覆盖。
以上的流程示意图如下图:
关键代码在于HashMap的put代码,详解如下图:
HashMap 是如何导致死循环的?
说到死循环都是说jdk7的hashmap的死循环,主要在多线程情况下,多个线程对map的同时扩容造成的,jdk7扩容的关键代码如下图:
可以看到jdk7比上一篇文章分析的jdk8的扩容的关键代码就简单多了,实际上就是遍历所以元素,找到在新的table下的位置,并把新table对应位置的元素设置成当前元素的next,就是所谓的头插法!
那么为什么会造成死循环呢?这里简单做一个造成死循环的流程分析,如下图:
造成死循环的原因主要在于线程1执行单key=9,并取出他的next(key=25)作为下次遍历后,线程1交出了执行权休息去了,而接着线程2吭哧吭哧的执行完扩容,扩容由于采用头插法,此时key=25那个Entry对象的next是key=9。
所以当线程1继续执行,到遍历next(key=25)时他的next变成了key=9,最终造成了两个Entry互相引用,如果后面调用map.get(41)就会刚好查询table[9],然后遍历这个链表,造成死循环!
总结
hashmap面试时主要的问题就是上面的那些了,有些可能没有覆盖到,但是只要弄懂了源码,所有问题都差不多,注意jdk8只是解决了死循环的问题,多线程下数据丢失的问题还是没有解决,所以多线程还是用ConcurrentHashMap。
有时候面试的时候可能面试官只会问“你了解hashmap吗?”,这种问题就不要想简单了直接回答一个了解就完了,而是把你理解的比如hashmap结构、主要方法实现、jdk7与jdk8的区别、jdk7可能的问题等都可以主动说出来,这样会更好!
Java程序员日常学习笔记,如理解有误欢迎各位交流讨论!