众所周知啊,jdk1.7和jdk1.8的扩容流程是不一样的,毕竟1.7在并发扩容的情况下可能会发生循环链表,从而导致get方法可能死循环,这个问题在jdk1.8的情况下已经解决了。今天我们来从源码来探究探究他是如何解决的以及一些其他的细节的不同
直接上代码:
final Node<K,V>[] resize() {
// 获得当前的Node数组
Node<K,V>[] oldTab = table;
// 获得当前Node数组的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 获得当前扩容时的阈值
int oldThr = threshold;
int newCap, newThr = 0;
// 如果当前Node数组长度大于零(这边也就是判断是否已经初始化过了)
if (oldCap > 0) {
// 如果已经达到最大的容量
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 如果当前容量乘2倍之后小于最大的容量并且当前容量大于等于默认容量(也就是16)
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 新的扩容阈值等于旧的阈值乘2
newThr = oldThr << 1;
}
else if (oldThr > 0)
newCap = oldThr;
else {
// 第一次初始化 懒加载
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;
// 初始化新的Node数组,也就是之前的两倍
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
// 先将新的赋值给table成员变量
table = newTab;
// 如果旧的不为null 也就是不是第一次初始化
if (oldTab != null) {
// 遍历每个Node数组
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;// helo GC
// 如果当前就一个节点
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 {
// 实现定义好两对 头尾指针
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
// 获得next节点
next = e.next;
// 如果当前遍历的这个e节点的hash值和旧的Node数组容量做按位与运算等于0
if ((e.hash & oldCap) == 0) {
// 就把这种情况放在lo这个链表里面
if (loTail == null)
loHead = e;
else
// 这边看得出来是尾插法
loTail.next = e;
loTail = e;
}
// 另外一种情况旧放在hi这个链表里面
else {
if (hiTail == null)
hiHead = e;
else
// 依旧是尾插法
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 判断lo链表是否不为null
if (loTail != null) {
// 为了保险 尾指针的next指向了null
loTail.next = null;
// lo链表就放在新的数组的下标为j的地方
newTab[j] = loHead;
}
// 判断hi链表是否不为null
if (hiTail != null) {
// 为了保险 尾指针的next指向了null
hiTail.next = null;
// hi链表就放在新的数组的下标为(j + 旧的数组的长度)的地方
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
以上也就是这个扩容源码的分析
下面总结一下:
1.7 是先扩容在添加新节点 1.8之后是先添加后扩容
1.7 采用的是头差法 1.8是尾插法
1.7 扩容后会对每个节点重新计算一次下标的位置 1.8发现 e节点的hash值和旧的Node数组容量做按位与运算等于0时那么在扩容后位置不变,否则新的位置的下标则是:旧的下标 + 旧的长度
即使是1.8的HashMap线程依旧是不安全的,首先就是对元素个数计数在并发的情况下就会发生少计,更别说还会出现数据覆盖问题
以上就是本篇文章的内容
下次我们介绍ConcurrentHashMap来具体看看他是如何解决的