链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。当数组中存储元素大于75%时,也会触发扩容。
JDK1.7的扩容方式的缺点:JDK1.7中新数组中链表采取头插法,得到一个逆序的链表。在多线程时有可能出现环形链表,大概原因是:线程1准备处理节点,线程2把HashMap扩容成功,链表已经逆向排序,那么线程1在处理节点时就可能出现环形链表。
JDK1.8扩容时使用的是resize()方法,该方法如下,对主要步骤进行了标识。
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;
}
//方法的四个参数分别是:当前hashMap对象、新数组、正在遍历的旧数组下标、旧数组的长度
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this;
// Relink into lo and hi lists, preserving order
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
int lc = 0, hc = 0;
for (TreeNode<K,V> e = b, next; e != null; e = next) {
next = (TreeNode<K,V>)e.next;
e.next = null;
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null)
loHead = e;
else
loTail.next = e;
loTail = e;
++lc;
}
else {
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}
//不为空时说明转换还没有结束
if (loHead != null) {
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}
基本步骤和原理如下;
- 判断是否满足扩容条件,根据情况选择扩容机制。正常情况下,通过移位运算,将新的容量和扩容阈值设为旧的两倍。循环原Table,将桶放到新的Table中。通过判断桶内元素的存储情况,对单个结点、红黑树、链表采取不同的处理方式。
- 对于单个结点:直接计算该结点在新数组中的位置。
- 对于链表:因为在存入时,采用的是(n-1)&hash的方式进行散列。所以通过hash&n, (因为散列方式,决定了位置的地址的长度不会超过数组的长度)结果若为0,则hash的高位也为0,则当进行移位运算后,用同样的方式散列位置相同, 若结果为1,则hash的高位为1,用同样的方式散列后,得到得结果高位多个1,即位置在原来的两倍的位置。
- 对于红黑树:用hash&bit(bit是原数组长度)等于0时(原理链表中已说明),则将该树链表头节点放到新数组时的索引位置等于其在旧数组时的索引位置,记为低位区树链表lo。不等于0时,则将该树链表头节点放到新数组时的索引位置等于其在旧数组时的索引位置再加上旧数组长度,记为高位区树链表hi。用lc和hc记录低位区和高位区的树链表元素个数。当桶中所有元素都处理完成后,lc/hc若小于等于6,则通过untreeify()去树化转换成链表,否则通过treeify()将之树化成红黑树