HashMap
说说HashMap的工作原理
从一下3点谈起:
- 底层数据结构
- 方法实现原理,以put方法举例
- HashMap中的特定参数等
- 不同JDK版本中HashMap实现方式的差异
HashMap底层结构
HashMap的底层结构为数组+链表或红黑树,当链表长度大于等于默认阈值8并且当前HashMap的容量大于等于MIN_TREEIFY_CAPACITY(默认64)的要求,就会把链表转成
红黑树结构;如果后续调整大小,当红黑树的节点小于等于6以后,又会恢复链表结构。
JDK1.7和JDK1.8中HashMap的区别
底层结构不同,JDK1.7中并没有红黑树的结构,全部都是链表;
在链表中插入数据的方式不同,JDK1.7中采用的是头插法(并发时,头插法会产生循环链表进而导致死循环的问题),JDK1.8中采用的是尾插法。
扩容的方式不同,JDK1.8中进行了优化;在JDK1.7中,HashMap扩容的时候需要对原数组中的元素进行重新Hash定位在新数组中的位置,在JDK1.8中采用的是原位置不变或者位置变为索引+旧容量大小。因为我使用的是2次幂的扩展,所以元素的位置要么在原位置,要么是在原位置再移动2次幂的位置,这样既省去了重新计算hash值得时间,而且由于新增的1bit是0还是1是随机的,因此resize的过程也均匀地把之前冲突的节点分散到新的bucket了。
为什么Map中的节点超过8个时才转换成红黑树?
在使用分布良好的用户的hashCodes时,很少使用红黑树结构,因为在理想情况下,链表的节点频率遵循泊松分布(意思就是链表各个长度的命中率依次递减),当命中第8次的时候,链表的长度是8,此时的概率仅为0.00000006,小于千万分之一。
当用户自己实现了不好的哈希算法而导致链表过长,影响查询效率,而此时通过转换红黑树结构用来保证极端情况下的查询效率。
HashMap的初始化和加载因子
HashMap的默认初始化大小是16,加载因子是0.75,扩容的阙值就是12(16*0.75),如果进行HashMap初始化的时候传入了自定义容量大小参数size,那么初始化的大小就是正好大于size的2的整数次方,比如传入10,大小就是16,传入30大小就是32。
之所以HashMap初始化的容量一定是2的整数次幂,是为了在计算出Hashcode之后,可以直接与容量大小进行与操作,将Hashcode的高位置0,保留低位然后直接用作数组下标进行访问。
HashMap为什么线程不安全?
首先HashMap就不是为并发操作设计的Map,Java中有专门的线程安全的Map如ConcurrentHashMap;然后HashMap中没有使用锁,有很多++modCount操作都不保证原子性;然后在并发执行put操作的时候,会导致数据丢失等。