目录
二、正常的 resize 过程 【其实就是每次先把next存下,然后对e(当前节点)重hash】
转载自:https://www.jianshu.com/p/619a8efcf589
一、resize()简介
新建一个更大尺寸的 hash 表,然后把数据从老的 Hash 表中迁移到新的 Hash 表中
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
......
// 创建一个新的 Hash Table
Entry[] newTable = new Entry[newCapacity];
// 将 Old Hash Table 上的数据迁移到 New Hash Table 上
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
迁移的源代码:该方法实现的机制就是将每个链表转化到新链表,并且链表中的位置发生反转,而这在多线程情况下是很容易造成链表回路,从而发生 get() 死循环。所以只要保证建新链时还是按照原来的顺序的话就不会产生循环(JDK 8 的改进)
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
//下面这段代码的意思是:
// 从OldTable里摘一个元素出来,然后放到NewTable中
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
二、正常的 resize 过程 【其实就是每次先把next存下,然后对e(当前节点)重hash】
- 假设我们的 hash 算法就是简单的用 key mod 一下表的大小(也就是数组的长度)
- 最上面的是 old hash 表,其中的 Hash 表的 size = 2,所以 key = 3, 7, 5,在 mod 2 以后都冲突在 table[1] 这里了
- 接下来的三个步骤是 Hash 表 resize 成 4,然后所有的 <key, value> 重新 rehash 的过程
三、并发下的 resize
1)假设有两个线程
do {
Entry<K,V> next = e.next; // 假设线程一执行到这里就被调度挂起了
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
而线程二执行完成了。于是有下面的这个样子:【线程1和线程2里各有一个newTable哈,它们完成resize后会让 table = newTable】
注意,因为 Thread1 的 e 指向了 key(3),而 next 指向了 key(7),其在线程二 rehash 后,指向了线程二重组后的链表。可以看到链表的顺序被反转
2)线程一被调度回来执行
- 先是执行 newTalbe[i] = e;
- 然后是 e = next,导致了 e 指向了 key(7)
- 而下一次循环的 next = e.next 导致了 next 指向了 key(3)
3)线程一接着工作。把 key(7) 摘下来,放到 newTable[i] 的第一个,然后把 e 和 next 往下移
4)环形链接出现
e.next = newTable[i] 导致 key(3).next 指向了 key(7)
此时的 key(7).next 已经指向了 key(3), 环形链表就这样出现了
总结与JDK 8 的改进
JDK 8 中采用的是位桶 + 链表/红黑树的方式,当某个位桶的链表的长度超过 8 的时候,这个链表就将转换成红黑树
HashMap 不会因为多线程 put 导致死循环(JDK 8 用 head 和 tail 来保证链表的顺序和之前一样(拆分成两条链表,再分别挂到 i 和 i+oldCap 位置 );JDK 7 rehash 会倒置链表元素),但是还会有数据丢失等弊端(并发本身的问题)。因此多线程情况下还是建议使用 ConcurrentHashMap
为什么线程不安全
HashMap 在并发时可能出现的问题主要是两方面:
- 如果多个线程同时使用 put 方法添加元素,而且假设正好存在两个 put 的 key 发生了碰撞(根据 hash 值计算的 bucket 一样),那么根据 HashMap 的实现,这两个 key 会添加到数组的同一个位置,这样最终就会发生其中一个线程 put 的数据被覆盖
- 1.7 (实际是Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e);) 大致意思就是e.next=table[i];table[i] =e;
- 同样1.8 在尾部插入的时候同时进行也会覆盖另一个线程的put
- 如果多个线程同时检测到元素个数超过数组大小 * loadFactor,这样就会发生多个线程同时对 Node 数组进行扩容,都在重新计算元素位置以及复制数据,但是最终只有一个线程扩容后的数组会赋给 table,也就是说其他线程的都会丢失,并且各自线程 put 的数据也丢失【这是1.8,如果是1.7头插还会导致死循环】
- 1.8对table[i]是拆分成两个链表,再挂到table[i]和table[i+oldCap]下,一个线程拆分j位置会让 oldTab[j] = null; 另一个线程可能就认为该位置无元素,该线程把它扩容后的数组赋给table,就会丢失其他线程的扩容
- 如果扩容的同时也put元素,比如put正在迁移的位置,那么因为最后会把拆分的两个链表赋给那两个位置,所以会导致各自线程put的数据也丢失。