0. 这一块涉及到的数据结构有点多…
Object.hashCode 算法
String.hashCode 算法
jdk 1.7 hashmap 并发下链表成环
jdk 1.8 hashmap 并发问题
hashmap 综合面试题
1. Hash
哈希算法可以将任意长度的二进制值引用为较短的且固定长度的二进制值,把这个小的二进制值称为哈希值。
1.1 hashCode() & equals()
不同对象的hashCode可能相同,因此覆写equals(),必须覆写hashCode()
1.2 Object.hashCode()
native实现方法,默认返回一个跟内存地址相关的字符串
可以在jvm启动参数中配置hashCode的生成策略
1.3 以String.hashCode()源码为例
此时可以看到String的hash是int类型=>4字节=>32位
static final int hash(Object key) {
int h;
//将高16位^低16位
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
1.4 为什么使用’^‘? 为什么是’>>>'?
一切都是为了散列分布 以及 cpu计算效率 考虑
'^'?
位运算符,操作系统支持,效率更好
相比于其他的位运算符,可以得到更好的散列效果(两个位相同为0,相异为1)
'>>>'?
'>>>'是无符号位运算符的右移,hashCode存在正负(毕竟int类型)
右移后,高16位hash的值(特征)对准低16位(位运算下,不足的高位补0)
2. HashMap 概述
底层是Entry[],Entry中有key,value,next,通过next指向下一个Entry构成链表结构
数组中的索引是通过key的hashCode以及数组长度计算得到的
默认,数组长16,负载因子0.75
jdk 1.7 数组元素称之为Entry,1.8 改为Node
1.8 当数组中某一个索引上的元素数量超过一定值后,将自动的把该索引上的链表结构替换成红黑树
3. HashMap reSize
过程:
扩容:创建一个新的Entry空数组,长度(所有元素,不是有挂载元素的数组元素)是原来的两倍
ReHash:遍历原来的Entry数组,把所有的Entry重新Hash到新数组。因为长度扩大之后Hash的规则会发生变动,因此需要重新Hash。
如果插入相同hashCode的元素(hash冲突),将形成链表结构以完成插入
触发条件:
HashMap.Size >= Capacity(容量) * LoadFactor(负载因子)
链表插入底层实现:
jdk 1.7 头插法
jdk 1.8 尾插法
4. HashMap 并发导致问题
4.1 以jdk 1.7 头插法的reSize实现源码为例:
/**
* Transfers all entries from current table to newTable.
*/
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) { //迭代Entry数组
while(null != e) { //该Entry是否有子节点(迭代entry所在的链表)
Entry<K,V> next = e.next; //子节点(相当于子节点往后的那条链表)
if (rehash) { //如果map需要重新计算hash(扩容之后)
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity); //Entry处在扩容后的新数组中的索引
//头插法------------------------
e.next = newTable[i]; //先取到 新数组索引上的元素(null或之前插入的链表)
newTable[i] = e; //把当前迭代的元素放到新数组索引上(通过e.next得到之前插入的链表,故插入到链表的头部)
//-------------------------头插法
e = next; //将当前迭代元素e的引用替换成e原来的next引用(向尾部迭代原数组中的链表)
}
}
}
4.2 链表环的形成分析
更加详尽的图文过程见文章顶部链接
并发下头插法将导致查询时,可能导致链表成环,程序进入死循环
因为多线程中被中断(但已进入到transfer()方法中)的线程被唤醒了,可能还认为新数组是刚扩容完的空数组
实则新数组中已存在链表元素,导致链表的尾元素的next指向了头元素(而不再指向null)
因为新数组在被中断的线程开始rehash时某个索引就不再指向null,而是指向一个链表元素了
5. HashSet
底层就是个HashMap<E,new Object>