Q1:HashMap的内部数据结构
JDK1.8版本之前是数组+链表,1.8版本之后是数组 + 链表+红黑树
数据结构图:
Q2:HashMap初始容量大小
如果 new HashMap()
不传值,默认大小是16,负载因子是0.75, 如果自己传入初始大小k,初始化大小为第一个 大于k的 2的整数次方,例如 如果传10,大小为16
//传入初始大小后 计算HashMap容量的方法 static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
Q3:HashMap的哈希函数
hash函数是先拿到 key 的hashcode,是一个32位的int值,然后让hashcode的高16位和低16位进行异或操作
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
为什么要这样设计?
hashcode右移16位,正好是32bit的一半,自己的高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性,而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。
这个也叫扰动函数(或者扰动计算),这么设计有两点原因:
- 降低hash碰撞,越分散越好;
- 因为这是高频操作, 因此采用位运算,更加高效;
Q4:如何获取数据存储位置下标
对数组的长度取模运算,得到的余数才能用来访问数组下标,源码中模运算就是把散列值(hash)和 数组长度-1 (length-1) 做一个"与"操作,位运算比取余%运算要快
数组下标 i = h & (length-1)
这也正好解释了为什么HashMap的数组长度要取2的整数幂。因为这样(数组长度-1)正好相当于一个“低位掩码”,“与”操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组下标访问。
以初始长度16为例,16-1=15。2进制表示是00000000 00000000 00001111,和某散列值做“与”操作如下,结果就是截取了最低的四位值,
10100101 11000100 00100101
& 00000000 00000000 00001111 ---------------------------------- 00000000 00000000 00000101 //高位全部归零,只保留末四位
Q5:HashMap数据插入流程图
1、判断数组是否为空,为空进行初始化;
2、不为空,计算 k 的 hash 值,通过(n - 1) & hash计算应当存放在数组中的下标 index;
3、查看 table[index] 是否存在数据,没有数据就构造一个Node节点存放在 table[index] 中;
4、存在数据,说明发生了hash冲突(存在二个节点key的hash值一样), 继续判断key是否相等,相等,用新的value替换原数据(onlyIfAbsent为false);
5、如果不相等,判断当前节点类型是不是树型节点,如果是树型节点,创造树型节点插入红黑树中;(如果当前节点是树型节点证明当前已经是红黑树了)
6、如果不是树型节点,创建普通Node加入链表中;判断链表长度是否大于 8并且数组长度大于64, 大于的话链表转换为红黑树;
7、插入完成之后判断当前节点数是否大于阈值,如果大于开始扩容为原数组的二倍(先插入最后扩容的缺点:有可能扩容之后后续就没有数据插入了,浪费,扩容很消耗性能)。
Q6:JDK1.8做了哪些优化及原因
1、数组+链表改成了数组+链表或红黑树;
原因:防止发生hash冲突,链表长度过长,查询效率低,修改为红黑树后将时间复杂度由O(n)
降为O(logn),
2、链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,JDK1.7将新元素放到数组中,原始节点作为新节点的后继节点,JDK1.8遍历链表,将元素放置到链表的最后;
原因:1.7头插法扩容时,会使链表发生反转,多线程环境下会产生环
3、扩容的时候1.7需要对原数组中的元素进行重新hash定位在新数组的位置,1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小;
原因:由于扩容是扩大为原数组大小的2倍,用于计算数组位置的掩码仅仅只是高位多了一个1,怎么理解呢?
扩容前长度为16,用于计算(n-1) & hash 的二进制n-1为0000 1111,扩容为32后的二进制就高位多了1,为0001 1111。
因为是& 运算,1和任何数 & 都是它本身,那就分二种情况,如下图:原数据hashcode高位第4位为0和高位为1的两种情况:
第四位高位为0,重新hash数值不变,第四位为1,重新hash数值比原来大16(旧数组的容量)
4、在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容;
Q7:HashMap线程安全么
不安全,在多线程环境下,JDK1.7 会产生死循环、数据丢失、数据覆盖的问题,JDK1.8 中会有数据覆盖的问题
HashTable、Collections.synchronizedMap、ConcurrentHashMap 是线程安全的
1、HashTable是直接在操作方法上加synchronized关键字,锁住整个数组,粒度比较大;
2、Collections.synchronizedMap是使用Collections集合工具的内部类,通过传入Map封装出一个SynchronizedMap对象,内部定义了一个对象锁,方法内通过对象锁实现;
3、ConcurrentHashMap使用分段锁,降低了锁粒度,让并发度大大提高
—————————————————————————————
感谢 https://blog.csdn.net/zhengwangzw/article/details/104889549