1、HashMap的node
HashMap类有一个非常重要的属性Node<K,V>是HashMap的一个内部类,实现了Entry接口。本质上是一个映射。如下图:
static class Node<K, V> implements Map.Entry<K, V> {
final int hash; //key的hash值
final K key; //与你put进去的key一样
V value; //与你put进去的value一样
Node<K, V> next;//存放下一个节点
}
2、HashMap里面的返回值为什么不是key.hashCode()的返回值,而是key.hashCode()^(key.hashCode()>>>16)的返回值呢?
这样做的目的是为了减少hash的冲突概率,计算出hash值h只会,对h和h无符号右移16位做异或运算。实质上就是将底的16为与高的16位异或运算,这样参加计算后,能够减少hash冲突。
为什么要设计成高低16位异或呢?
因为 key.hashCode()函数调用的是 key 键值类型自带的哈希函数,返回 int 型散列值。int 值范围为很大,前后加起来大概 40 亿的映射空间。只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。你想,如果 HashMap 数组的初始大小才 16,用之前需要对数组的长度取模运算,得到的余数才能用来访问数组下标。异或运算还比取模快。
结果显示,当 HashMap 数组长度为 512 的时候(2929),也就是用掩码取低 9 位的时候,在没有扰动函数的情况下,发生了 103 次碰撞,接近 30%。而在使用了扰动函数之后只有 92 次碰撞。碰撞减少了将近 10%。看来扰动函数确实还是有功效的。
3、为什么HashMap要用数组+链表+红黑树实现?
使用数组的原因是因为数组获得元素的时间复杂度为o(1),链表是为了当发生hash冲突时,可以继续存放元素,红黑树是为了在极端情况下二叉树的不平衡。当数组长度大于64且链表长度大于8的时候,链表才会转红黑树。链表长度大于8,数据查找效率变慢,还有一个原因是链表长度大于8发生的概率很小。一开始不适用红黑树是因为红黑树左旋,右旋维护成本太高。
根据jdk代码能够发现,链表长度(也是发生hash碰撞的概率)概率如下图:
4、HashMap的put方法的大致实现流程
5、HashMap中数组大小为什么要是2的幂次方
因为只有当n为2的幂次方时,才满足hash%n=(n-1)&hash。按位&运算要比%取模快很多倍,十几倍。能保证索引值在范围中,不会超过数组长度。就算你给个长度为11,hashmap内部也会强制转换成你跟你给的值最接近,而且满足等于2的n次幂。
6、HashMap如何计算下标的=
HashMap中数组下标值的计算过程,大致分为如下几步:获取key.hashCode(),然后将hashCode高16位和低16位异或(^)操作,然后与当前数组长度-1结果进行与(&)操作,最终结果就是数组的下标值。
至于由于当前结点(键值对)的加入,导致当前HashMap中容量超过了阈值而扩容2倍,扩容后导致每个结点重新计算下标值(称为新下标值),新下标值只有两种可能:一、key的hash()返回值新加入与(&)操作计算的一位为0,则下标值与原下标值相同。二、key的hash()返回值新加入与(&)操作计算的一位为1,则计算出来的下标值=原下标值+原数组长度。如果key对象为Integer,他的hashCode()直接返回的value。
7、JDK1.8下的HashMap是如何解决线程安全问题的?
在多线程环境下,1.7 会产生死循环、数据丢失、数据覆盖的问题,1.8 中会有数据覆盖的问题,以 1.8 为例,当 A 线程判断 index 位置为空后正好挂起,B 线程开始往 index 位置的写入节点数据,这时 A 线程恢复现场,执行赋值操作,就把 A 线程的数据给覆盖了;还有++size 这个地方也会造成多线程同时扩容等问题。
通过扰动函数加高低位异或。
8、Java 中有 HashTable、Collections.synchronizedMap、以及 ConcurrentHashMap 可以实现线程安全的 Map。
HashTable 是直接在操作方法上加 synchronized 关键字,锁住整个数组,粒度比较大,Collections.synchronizedMap 是使用 Collections 集合工具的内部类,通过传入 Map 封装出一个 SynchronizedMap 对象,内部定义了一个对象锁,方法内通过对象锁实现;ConcurrentHashMap 使用分段锁,降低了锁粒度,让并发度大大提高。
9、那你知道 ConcurrentHashMap 的分段锁的实现原理吗?
ConcurrentHashMap 成员变量使用 volatile 修饰,免除了指令重排序,同时保证内存可见性,另外使用 CAS 操作和 synchronized 结合实现赋值操作,多线程操作只会锁住当前操作索引的节点。
如下图,线程 A 锁住 A 节点所在链表,线程 B 锁住 B 节点所在链表,操作互不干涉。
10、你前面提到链表转红黑树是链表长度达到阈值,这个阈值是多少?
阈值是 8,红黑树转链表阈值为 6
11、为什么是 8,不是 16,32 甚至是 7 ?又为什么红黑树转链表的阈值是 6,不是 8 了呢?
因为经过计算,在 hash 函数设计合理的情况下,发生 hash 碰撞 8 次的几率为百万分之 6,概率说话。。因为 8 够用了,至于为什么转回来是 6,因为如果 hash 碰撞次数在 8 附近徘徊,会一直发生链表和红黑树的转化,为了预防这种情况的发生。
12、有序的map
LinkedHashMap 内部维护了一个单链表,有头尾节点,同时 LinkedHashMap 节点 Entry 内部除了继承 HashMap 的 Node 属性,还有 before 和 after 用于标识前置节点和后置节点。可以实现按插入的顺序或访问顺序排序。
13、LinkedHashMap 和 TreeMap
TreeMap 是按照 Key 的自然顺序或者 Comprator 的顺序进行排序,内部是通过红黑树来实现。所以要么 key 所属的类实现 Comparable 接口,或者自定义一个实现了 Comparator 接口的比较器,传给 TreeMap 用户 key 的比较。
14、JDK1.7的HashMap的rehash底层如何实现的?
15、HashMap中的modcount表示什么意思?
部分内容参考了牧云君博客文章