目录
1.扩容形成环路
在put元素超过负载阈值时会触发HashMap的扩容resize操作,一个桶的链表会重新散列到新表中,
/**
* put 插入元素之后,负载超过阈值,触发resize方法扩容
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}
/**
* resize(),transfer把旧表中的元素添加到新表中
*/
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity]; // 创建新表
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
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);
// 插入到桶中链头
e.next = newTable[i];
newTable[i] = e; // 放入桶中链表头
e = next; // 链向原表头
} // while
}
}
操作 | 图 | 解释 |
---|---|---|
put | map.put(3,"A"); map.put(7,"B");插入链头 如图 有两个桶的table, threshold=cap*DEFAULT_LOAD_FACTOR=1 |
|
resize | 并发扩容, 如果线程B在拷贝新表第一行已经拿到next节点后发生切换, 线程A正常拷贝结束,插入队头 线程B切回继续拷贝,就会形成环路 且由于可见性问题,扩容操作的遍历可以结束,因为next的next在此时的高速缓存里是NULL。 |
|
get | 获取15时,会落在tab[2]里,会遍历链表,链表中没有命中,就会一直遍历下去(回头路)! |
2.get(key)操作死循环
HashMap在get操作一直在抢占CPU,按道理get操作是平均O(1)的,不太会造成这种现象
那是因为get操作需要遍历拉链后的链表,链表如果key不命中,就一直循环下去。