JDK7实现
当entry数组进行rehash扩容的时候会形成循环列表导致后续map.get(key)的时候陷入死循环当entry数组进行rehash扩容的时候会形成循环列表导致后续map.get(key)的时候陷入死循环。
直接上代码:
/**
* Transfers all entries from current table to newTable.
*/
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;
}
}
}
上面代码是entry数组扩容后将原来的数据村放入新的entry数组中
假设现在有A,B两个线程,同时向map中put元素,长度刚好到了扩容临界点,此时数组进行扩容。
线程1,线程2同时进入扩容,两个线程都会新建newTable。
线程1拿到cpu时间片执行transfer()方法之后newTable的结构为:
此时线程1尚未将newTable赋值给table,这时时间片切换,线程2拿到cpu执行权,进行操作,第一次循环结果:
此时next为b,b!=null,再次进入循环(第二次循环)
此时next为a,a!=null,第三次进入循环:
最终next为null,跳出循环。
节点a和b互相引用,形成了一个环,当在数组该位置get寻找对应的key时,就发生了死循环。
另外,如果线程2把newTable设置成到内部的table,节点c的数据就丢了,看来还有数据遗失的问题。
JDK8实现
java8中对hashmap进行了比较大的更改,加入了红黑树的实现等,我们先抛开这些,单纯就扩容的方法来进行探讨。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length; //旧数组长度
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//MAXIMUM_CAPACITY =1 << 30,最大值为2^30次方
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold 扩容原长度*2
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);//红黑树的逻辑跳过
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
//e.hash & oldCap 方法切断元素在扩容的时候是否需要移动位置
//17 & 16 ==1,说明在扩容的时候hash为17这个node需要移动位置
//12 & 16 ==0,说明在扩容的时候hash为12这个node不需要移动位置
//32 & 16 ==0,说明在扩容的时候hash为32的node不需要移动位置
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//循环的部分解决了1.7以前原链a-》b-》c rehash之后变成c->b->a的问题,从根本上解决了产生死链的问题
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
但是这并不保证hashmap就是线程安全的,在多线程的情况下hashmap还是不安全,可能会丢失数据。