引言
上一篇咱们分析了HashMap的put和get方法,大家应该对HashMap有个简单的了解,如果不了解得可以看上一篇文章。本文分析的重点是resize扩容方法,因为JDK8在这段代码上面对JDK7做了修改,现在我们一起来看看具体做了哪些修改,以及为什么要这么修改。
1、分析JDK7源码
新建一个更大尺寸的hash表,然后把数据从老的Hash表中迁移到新的Hash表中
void resize(int newCapacity)
{
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
......
//创建一个新的Hash Table
Entry[] newTable = new Entry[newCapacity];
//将Old Hash Table上的数据迁移到New Hash Table上
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
下面是迁移的代码:
void transfer(Entry[] newTable)
{
Entry[] src = table;
int newCapacity = newTable.length;
//下面这段代码的意思是:
// 从OldTable里摘一个元素出来,然后放到NewTable中
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
我们都知道HashMap是非线程安全的,上面这段代码在多线程的情况下会导致HashMap死循环,我们来演示下这个过程,最上面的是old hash 表,其中的Hash表的size=2, 所以key = 3, 7, 5,现在要进行扩容了,假设我们有两个线程,线程二执行完成了:
线程一现在开始执行,先是执行 newTalbe[i] = e,然后是e = next,导致了e指向了key(7),而下一次循环的next = e.next导致了next指向了key(3)
再看下一流程,也正常
环形链接出现,e.next = newTable[i] 导致 key(3).next 指向了 key(7)
注意:此时的key(7).next 已经指向了key(3), 环形链表就这样出现了。
2、分析JDK8源码
由于resize的方法比较长,我先说下具体的思路,然后再贴源码,大家就更加清楚了:
- 如果table == null, 则初始化threshold值, 生成空table返回
- 如果table不为空, 需要重新计算table的长度, newCap= oldCap<< 1(扩容为原来的2倍, 如果原oldCap已经到了上限, 则newCap= oldCap)
- 遍历oldTable,首节点不为空则进入下一步,为空则本次循环结束。
- 无后续节点, 重新计算hash位, 并赋值,本次循环结束
- 当前是红黑树, 走红黑树的重定位
- 当前是链表, JAVA7时还需要重新计算hash位, 但是JAVA8做了优化, 通过(e.hash & oldCap) == 0来判断是否需要移位; 如果为真则在原位不动, 否则则需要移动到当前hash槽位 + oldCap的位置
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;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
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) {
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);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
3、总结
- JDK8对HashMap死循环的解决方法是:扩容后,新数组中的链表顺序依然与旧数组中的链表顺序保持一致。
- JDK8虽然修复了死循环的bug,但是HashMap 还是非线程安全类,仍然会产生数据丢失等问题,线程安全类还是使用ConcurrentHashMap
结束语
本篇至此终于写完了,关于ConcurrentHashMap的介绍会晚一点写,下一篇的内容将会分析HashSet
如果本篇内容对你有所帮助,请随手点个赞,谢谢大家!
转载:https://blog.csdn.net/cool_summer_moon/article/details/105731952
查看评论