HashSet理解(一)java集合
HashSet理解(二)怎么做到值不重复
HashSet理解(三)add方法(jdk1.7及以前)是如何插值的
HashSet理解(四)为什么jdk1.7中的头插法会形成环和死循环?
jdk1.7中,多线程环境下,扩容时,单链表可能会产生环,导致死循环。
jdk1.7 put方法,没变
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
扩容的过程:从addEntry()到transfer()
之前的文章都分析的是jdk1.6,jdk1.6和jdk1.7u的addEntry()方法有区别,但区别不大。看看1.7的扩容:
//jdk7中,仍然是先插入,再扩容
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}
//jdk7u60中,改为了先扩容再插入
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
}
//头插
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
threshold
是一个临界值,等于16*0.75=12,size
是所有entry的数量,并不是数组table[]的实际长度,在createEntry()中会执行size++;
。所以HashMap扩容的两个条件:
- entry的总数量大于等于阈值
- 当前插入的entry,发生了hash碰撞
再说的直白一些,扩容前,数组table[]可能存在空元素,例如table[0]上有6个entry,table[1]上有5个entry,这时再向table[1]上面插入一个元素,就会扩容。但是此时,table[2],table[3]都是空的。
看看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循环永远不会退出,就出现了死循环。