前言
JDK1.7由于采用的头插法,所以多线程情况下可能会产生死循环问题。
正文
头插法
就是每次从旧容器中的hash桶中取出数据后,放到新容器的头节点问题,如果此时头结点位置为空,直接放置即可,如果不为空将头节点的数据往后挪一位,让出位置给迁移节点。
容器迁移
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, initHashSeedAsNeeded(newCapacity));
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;
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;
}
}
}
迁移主要的思路是,遍历当前hash桶数组,从每个桶的头节点遍历,挨个迁移到新的组数的头节点,并且原来新节点的头节点往后移动;以上代码在多线程下可能产生死循环问题,下面分析下为什么会产生。
死循环分析
1、假设现在需要迁移的旧集合结构如下:
2、假设现在有两个线程1和线程2,由于他们都会创建新的容器,所以此时的结构如下:
3、假设线程2执行到此代码片段后被挂起了,恰好e=A,那next=B了,我们记住线程2的状态哈
4、线程1先获得CPU的执行权,当执行完成后的结构如下:
5、由步骤4可知,此时的A.next=null,B.next=A,此时的状态是正常的。线程2开始获取CPU的执行时间,由于每个线程都创建了一个新的容器,它此时的状态为步骤2中图片所示,线程2开始工作后,从步骤3中的代码段开始执行,由于代码段中保存了e=A,next=B,所以会先进行A的迁移,A迁移完成后由于代码中保存了next,所以还会对B进行迁移。
6、步骤5中还未形成死循环,由于线程1中迁移完成造成,B.next=A,所以代码块会判断B.next是否为null,不为null则拿出下个值进行迁移,由于next=A,所以又将A迁移到新的容量中去了。此时A.next=B,此时就真正形成了闭环。又对A进行判断是否有next…不断添加