特点
- 初始size为16,扩容:newsize = oldsize*2,size一定为2的n次幂
- 扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入
- 插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容)
- 当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀
- 计算index方法:index = hash & (tab.length – 1)
数据结构
JDK7中HashMap采用的是位桶+链表的方式
JDK8中采用的是位桶+链表/红黑树的方式
当某个位桶的链表的长度超过8的时候,这个链表就将转换成红黑树。因为引入了树,所以其他操作也更复杂了,比如put方法以前只要通过hash计算下标位置,判断该位置有没有元素,如果有就往下遍历,如果存在相同的key就替换value,如果不存在就添加。但是到了8以后,就要判断是链表还是树,如果是链表,插入后还要判断要不要转化成树。不过这些操作都是常量级别的,复杂度还是O(1)的,但是对整体性能提升非常大。链表转换红黑树在treeify方法里实现,给树插入节点在puttreeval方法,修正红黑树是balanceInsertion方法。
resize扩容方法
jdk7里hashmap resize时对每个位桶的链表的处理方式(transfer方法),整体过程就是先新建两倍的新数组,然后遍历旧数组的每一个entry,直接重新计算新的索引位置然后头插法往拉链里填坑
jdk8的代码里是把链表上的键值对按hash值分成lo和hi两串,lo串的新索引位置与原先相同[原先位置j],hi串的新索引位置为[原先位置j+oldCap]。这么做的原因是,我们使用的是2次幂的扩展(newCap是oldCap的两倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置,也就是原索引+oldCap。为啥
Node
Hashmap的底层实现是使用一个entry数组存储,默认初始大小16,不过jdk8换了名字叫node,可能是因为引入了树,叫node更合适。
JDK7中HashMap扩容机制的死循环问题
(put方法里,有个addEntry()新建enty的方法,在这个方法里,调用了resize()扩容方法)
//扩容方法
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {//最大容量为 1 << 30
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];//新建一个新表
boolean oldAltHashing = useAltHashing;
useAltHashing |= sun.misc.VM.isBooted() &&
(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean rehash = oldAltHashing ^ useAltHashing;//是否再hash
transfer(newTable, rehash);//完成旧表到新表的转移
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) { //遍历同桶数组中的每一个桶
while(null != e) { //顺序遍历某个桶的外挂链表
Entry<K,V> next = e.next;//引用next
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);//找到新表的桶位置;原桶数组中的某个桶上的同一链表中的Entry此刻可能被分散到不同的桶中去了,有效的缓解了哈希冲突。
e.next = newTable[i];//头插法插入新表中
newTable[i] = e;
e = next;
}
}
由于缺乏同步机制,当多个线程同时resize的时候,某个线程t所持有的引用next,可能已经被转移到了新桶数组中,那么最后该线程t实际上在对新的桶数组进行transfer操作。如果有更多的线程出现这种情况,那很可能出现大量线程都在对新桶数组进行transfer,那么就会出现多个线程对同一链表无限进行链表反转的操作,极易造成死循环,数据丢失等等