HashSet理解为什么jdk1.7中的头插法会形成环和死循环?
Posted zhangjin1120
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HashSet理解为什么jdk1.7中的头插法会形成环和死循环?相关的知识,希望对你有一定的参考价值。
HashSet理解(一)java集合
HashSet理解(二)怎么做到值不重复
HashSet理解(三)add方法(jdk1.7及以前)是如何插值的
HashSet理解(四)为什么jdk1.7中的头插法会形成环和死循环?
jdk1.7中,多线程环境下,扩容时,单链表可能会产生环,导致死循环。
扩容的过程:从addEntry()到transfer()
之前的文章都分析的是jdk1.6,jdk1.6和jdk1.7的addEntry()方法有区别,但区别不大。看看1.7的扩容:
void addEntry(int hash, K key, V value, int bucketIndex) {
//当size大于等于某一个阈值thresholdde时候且该桶并不是一个空桶;
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);//将容量扩容为原来的2倍,也就是32
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);//扩容后的,该hash值对应的新的桶位置
}
createEntry(hash, key, value, bucketIndex);//在指定的桶位置上,创建一个新的Entry
}
看看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;
//注意这里是table,不是newTable
//从0到table.length-1,依次遍历旧数组table
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);
}
//找到新数组的下标;
//原桶数组中的某个桶上的同一链表中的Entry此刻可能被分散到不同的桶中去了,有效的缓解了哈希冲突。
int i = indexFor(e.hash, newCapacity);
//头插法
//第一次插入时,newTable[i]是null
//第二次插入时,newTable[i]是第一次插入的e
e.next = newTable[i];
newTable[i] = e;
e = next; //while循环继续向下走
}
}
}
单线程下,旧表中的数据,是如何转移到新表的?
把头插法的核心代码罗列出来,其他代码暂时去掉如下:
while(null != e) {
Entry<K,V> next = e.next; //t2线程,执行到这里挂起
e.next = newTable[i]; //1
newTable[i] = e; //2
e = next;
}
-
画个a-b-null的单链表,作为旧表的table[1],那么第一轮循环,e就是a,next就是b 。如下图:
-
e.next=newTable[i];
就是a指向新表的头结点,头结点现在还是null。a与b的连接断开了。如下图:
-
newTable[i]=e;
头结点直接等于a,a进入新表。结果如下图:
-
e=next;
e变为b, 继续执行e.next=newTable[i];
,就是b指向新表的头结点a。结果如下图:
-
继续执行
newTable[i]=e;
e是b, 头结点直接等于b。这是就形成了b-a-null的新的单链表。如下图。单线程通过头插法,把旧表的数据转移到扩容后的新表,就是这个过程。
多线程情况下,单链表的环是怎样形成的?死循环又是怎么回事?
假设两个线程t1,t2同时扩容,同时转移数据。t1,t2都执行完下面的第1行代码,这时,t2挂起,t1线程继续走上面的转移数据流程。
while(null != e) {
Entry<K,V> next = e.next; //1 t2线程,执行到这里挂起
e.next = newTable[i]; //2
newTable[i] = e; //3
e = next; //4
}
等到t1走完上述流程后,t2开始执行第2行代码,这时t2线程挂起前,保存的e还是a, next是b。执行e.next=newTable[i];
就是令a指向b,这时a,b就形成了环。结果如下:
程序还没有结束,e=next;
next是b, 不为null, while循环继续执行。
e.next = newTable[i];
newTable[i] = e;
e = next;
明显,a的next是b,b的next是a,所以e一直都不会为null,while循环永远不会退出,就出现了死循环。
以上是关于HashSet理解为什么jdk1.7中的头插法会形成环和死循环?的主要内容,如果未能解决你的问题,请参考以下文章