提前预知
JDK1.7中HashMap的数据结构是:数组+链表,如下图:
HashMap中扩容时转移数据的方法
- transfer方法:
扩容时就是调用这个方法来转移数据的
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//遍历table数组
for (Entry<K,V> e : table) {
//如果table数组中的节点不为空,继续判断是不是链表结构
while(null != e) {
// 获取到当前节点的下一个节点
Entry<K,V> next = e.next;
// 判断是否需要重新hash
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
// 根据hash值和新的数组容量找到索引
int i = indexFor(e.hash, newCapacity);
// 将头结点的下一个节点指向新节点的地址
e.next = newTable[i];
// 将新节点指向当前节点的地址
newTable[i] = e;
e = next;
}
}
}
单线程扩容转移数据
注意:如果是单线程扩容,是不会出现环形链表的,步骤如下图:
假设扩容前HashMap容量为4,扩容后为8
-
第一步:
-
第二步:
e.next = newTable[i];
-
第三步:
newTable[i] = e;
-
第四步:
e = next;
-
通过上面的步骤,第一个数据转移完毕,下面是第二个:
-
第三个数据转移:
通过上面的分析可以看出,单线程的数据转移是没有为题的,但是多线程
下面就不一样了。
多线程扩容转移数据
假设有两个线程进行可扩容,举个极端的例子,就像数学中的反证法一样,只要有一种情况能出现,就是线程不安全的。
- 线程1执行到这里,被线程2占用
- 线程2完成转移完成
- 线程1获得执行权,接着之前的执行,完成第一个数据转移,当进行下一个数据转移的时候要注意指针的移动方向,
e1指向next
,next指向链表的下一个节点
,这个时候它俩刚好要交换一下位置
。
- 完成二个数据转移
- 完成第三个数据转移,问题来了,出来一个环形链表。
总结
我举的是一个比较极端的例子,也就是在扩容的时候,整个链表都被完整的转移过来了,一般情况下是不会整条链表都转移过来,会被分散,这也是扩容的目的,使之前密集分布的数据散开。
虽然是个极端的例子,但还是有出现的概率的。
JDK7中多线程下对HashMap的扩容可能
会形成环形链表