Java Collection集合源码详解 —— Map(2)
- 作者:shiwyang 今天发现HashMap居然还有红黑树退化的现象,原来是还有几个方法没看完,今天在这里开第二篇补充一下
红黑树退化
- 在HashMap中,红黑树的结点个数当小于 6 的时候,会使得红黑树退化为原来的链表(具体原因在源码注释里面写了,利用泊松分布算出来的 8扩容 6退化),红黑树会在两种情况中判断是否需要退化
- 数组扩容时(数组扩容时,内部所有元素都需要重新确定索引值,因此一个数组中的桶元素可能会减少)
- 移除元素时(移除元素可能使桶内的元素小于6个,红黑树发生退化)
- 在内部有涉及树的退化的两个函数
- split() 数组扩容时候使用的
- removeTreeNode() 移除元素的时候使用的
数组扩容的红黑树退化
- 学习数组扩容时的红黑树退化,首先就应该学习HashMap是如何扩容的,具体到代码上就是 resize() 方法
- 在学习红黑树的退化之前,首先会遇到一个链表元素更新索引的问题,这个问题可以说是红黑树分裂和退化的基础,在了解了这个基础之上,我们才能够全面的认识红黑树的退化过程
resize()
-
最好先自己先研究一遍代码,看不懂的地方先看我下方的代码注释,还是看不懂再看上面的文字,这个又复杂又神奇的,很有意思!
-
resize函数是HashMap的扩容方法,在扩容的时候有两种情况
- 原table为空
- 原table内有元素
-
这个函数开头定义的每一个中间元素都是有意义的,每一个都不能缺
- oldTable : 用来将原来的table先保存起来,将table空间扩展之后,再将原来的元素迁移过去,有点类似与Arrays里面的copyof()
- oldCap :用来记录原数组的大小,后面在计算桶元素新索引的时候,需要用到这个数
- oldThr: 用来记录原扩容常数,用来计算新扩容常数和判断原数组状态的
- newCap、newThr: 用来保存新的数组大小 新的扩容常数,这两个变量可能会遇到超过范围的情况,超过范围有一个默认极大值,所以也需要单独拿出来
-
当原数组为空时,扩容返回即可
-
当原数组不为空时,扩容后数组内的桶都需要重新判断,桶有两种情况
- 链表:需要将链表内所有的元素重新计算索引值
- 红黑树:将红黑树使用spilt()重新分配,可能会涉及树的退化(重点)这个内容比较多,放在后面
-
当原数组位置的桶结构是一个链表时,链表内的元素计算索引之后可能会有两种情况:1)原位 2)原位 + 原数组大小
- 这里首先要了解,每一个元素是怎么算出这个索引值的?它是通过hash值和(Hash表数组长度 - 1)进行与运算(e.hash & (newCap - 1))可以得到一个不大于数组长度最大值的数,作为索引。列一个简单的计算过程复习一下与运算
与运算过程(同1得1) 例1 例2 hash值 11111010 10011010 HashMap数组长度 - 1 01111(16 -1) 01111(16-1) 结果 01010(十进制10) 01010(十进制10) - 所以上面两个例子在数组内都是一样的下标,在扩容的时候,进行e.hash & oldCap,实际上对比的就是最高位的差别,对比一位的差别,只可能会出现两种情况,因此也就可以分为高位和原位两种情况
- 至于为什么不用上面的计算方法呢,是因为上面的计算方法得到的是一个具体的索引值,而这种方法能够得到的是易于判断的数(0和其他)
与运算过程(同1得1) 例1 例2 hash值 11111010 10011010 HashMap数组长度 10000 10000 结果 10000(高位) 0(原位)
final Node<K,V>[] resize() {
// 用一个oldTab 先把目前的table 保存起来
Node<K,V>[] oldTab = table;
// 原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;
}
// 设定新的数组大小 和新扩容常数(计算出的数组大小必须小于int极值,还必须大于默认数组大小:10,如果没有,需要往下)
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 原数组为空 原扩容常数大于0时
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// 原数组为空 原扩容常数也为0 ,就是正常第一次添加元素的情况
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 当新扩容常数为0时
// 这个情况出现时,就是当原数组不为空,但是扩容之后的数组又超过int极值的情况
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 经过上方一系列的判断,可以最终确定 newCap newThr两个值
// 下方正式进入扩容阶段
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
// 建立一个新的 Node[]数组表,大小为newCap
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
// table 完成扩容 但是这时候原table的元素还在oldTab里面
table = newTab;
// 当原table不为空的时候,将老的table元素迁移到table里面
// 如果原table为空的时候,就可以直接返回了,这种情况一般是在第一次添加元素的时候遇到
if (oldTab != null) {
// 遍历老的table
for (int j = 0; j < oldCap; ++j) {
// 保存遍历数组的头节点,使用Node对象来存储
// 但是这个元素既可能是一个链表,用Node对象存储,也可能是红黑树,用TreeNode存储
// 但是TreeNode 也可以用Node 表示,称之为向上转型,多态的概念
Node<K,V> e;
// 取出数组的头节点
if ((e = oldTab[j]) != null) {
// 清空原table内的元素,应该是为了GC
oldTab[j] = null;
// 如果只有一个元素
if (e.next == null)
// e.hash & (newCap - 1) 这个计算式子可以算出新的索引位置,可能会和原索引值不同
// 可能会在原位置,或者原位置 + 数组长度位置,具体原因也在这个计算式子里面,用& 运算之后的得出的结果,非常神奇
newTab[e.hash & (newCap - 1)] = e;
// 如果e是TreeNode (instanceof 当对象是右边类或子类所创建对象时 返回true,向下转型,多态的概念)
else if (e instanceof TreeNode)
// 将树中的每一个元素重新放到他们对应的位置,这个函数里面就涉及到树的退化过程
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 如果e是一个链表表头,且不止有一个元素时
else { // preserve order
// 设置了两条新的链条 loHead hiHead
// 扩容后,数组内桶的每一个元素都需要重新计算一次索引值,确定定位位置
// 而同一个桶内的所有数据,在重新计算hash值之后,只有可能是在原位置或者(原位置 + 原数组长度)的地方
// 因为在和扩容常数-1的与运算中,只会涉及原位置 + 1位的计算,+1位的情况下只能有两种结果,这里我很难用文字表达出来,TODO:可能会在下面列一个计算索引的过程
// 所以在重新计算索引值只可能会出现两种情况的基础之上,只需要设置两个数组链,在全部计算完成之后,将整个链表加入对应的位置
// 算出新索引的最大用处是,可以降低hash冲突,减少桶深度,让原数组的各个桶的元素都均匀的分布在新的数组中,提高了搜索速度,提高了数组利用率
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
// 遍历链表的临时变量
Node<K,V> next;
// 循环判断每一个元素的索引,加入对应的链表
do {
next = e.next;
// hash和原数组大小进行与运算之后,就会在原数组最高位进行判断,会出现0和不等于0 两种情况,分别代表放在原位链表和高位链表
// 1)放在原位链表
if ((e.hash & oldCap) == 0) {
// 当链表还为空的情况
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 2) 放在高位链表
else {
// 当链表还为空的情况
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 判断完原链表内所有元素的位置之后,生成两个链表,将链表放在对应的位置
//1) 原位链表
if (loTail != null) {
// 结束链表末端
loTail.next = null;
newTab[j] = loHead;
}
// 2) 高位链表
if (hiTail != null) {
// 结束链表末端
hiTail.next = null;
// j + oldCap:原位+原数组长度
newTab[j + oldCap] = hiHead;
}
}
}
}
}
// 完成扩容!
return newTab;
}
虽然刚研究完上面那么一大堆,但其实对于红黑树退化只能算是前置知识,在扩容的时候,如果是红黑树,红黑树内的元素也会重定位,所以也有一个大概的过程,在重定位之后,发生数量不足的情况才会到我们要讨论的红黑树退化的过程,下面就详细的研究一下split这个函数的源码。
split()
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
// 使用两个红黑树来记录 loHead hiHead
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
// 两个变量分别记录树分裂后,各个树的大小,如果小于 6 就触发红黑树退化
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;
// 记录loTree 的元素数量
++lc;
}
else {
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
// 记录hiTree 的元素数量
++hc;
}
}
if (loHead != null) {
// 假如 loTree 的元素数量 < UNTREEIFY_THRESHOLD(6)
if (lc <= UNTREEIFY_THRESHOLD)
// 触发untreeify使红黑树退化
tab[index] = loHead.untreeify(map);
else {
// loTree 为原位
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
// 红黑树生成函数
loHead.treeify(tab);
}
}
if (hiHead != null) {
// 假如 hiTree 的元素数量 < UNTREEIFY_THRESHOLD(6)
if (hc <= UNTREEIFY_THRESHOLD)
// loTree 为原位 + oldCap
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
// 红黑树生成函数
hiHead.treeify(tab);
}
}
}
untreeify()
这个就是真正的红黑树退化的函数,但其实红黑树的退化过程很简单,只需要将TreeNode的向上转型为Node就行。
- 把TreeNode 转化为 Node
- 把Node链接起来
final Node<K,V> untreeify(HashMap<K,V> map) {
Node<K,V> hd = null, tl = null;
for (Node<K,V> q = this; q != null; q = q.next) {
// 向上转型
Node<K,V> p = map.replacementNode(q, null);
// 链接链表
if (tl == null)
// 头结点
hd = p;
else
// 链接每一个结点的next结点
tl.next = p;
tl = p;
}
// 返回头节点
return hd;
}
移除元素的红黑树退化
- 判断要删除的元素是否属于红黑树,如果是,先获取到树结点getTreeNode
- 获取到树节点,之后删除树结点removeTreeNode
removeNode()
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
// 如果要删除的数组位置上的桶是红黑树结构的元素
if (p instanceof TreeNode)
// 获得树结点内指定要删除的元素
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
// 如果找到了需要删除的元素
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
// 并且是红黑树元素
if (node instanceof TreeNode)
// 执行removeTreeNode函数
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
removeTreeNode()
- TODO:这个涉及红黑树的部分实在太复杂了,我还没搞懂,只能看懂简单逻辑
- 先找到需要删除的元素
- 找到删除的元素之后看看删除完还能不能保持红黑树的结构
- 能 -> 平衡红黑树结构 判断红黑树是否成立 返回二叉树根结点
- 不能 -> untreeify() 返回链表根结点
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
boolean movable) {
int n;
if (tab == null || (n = tab.length) == 0)
return;
int index = (n - 1) & hash;
TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
if (pred == null)
tab[index] = first = succ;
else
pred.next = succ;
if (succ != null)
succ.prev = pred;
if (first == null)
return;
if (root.parent != null)
root = root.root();
// 假如树空了,就untreeify
if (root == null || root.right == null ||
(rl = root.left) == null || rl.left == null) {
tab[index] = first.untreeify(map); // too small
return;
}
TreeNode<K,V> p = this, pl = left, pr = right, replacement;
if (pl != null && pr != null) {
TreeNode<K,V> s = pr, sl;
while ((sl = s.left) != null) // find successor
s = sl;
boolean c = s.red; s.red = p.red; p.red = c; // swap colors
TreeNode<K,V> sr = s.right;
TreeNode<K,V> pp = p.parent;
if (s == pr) { // p was s's direct parent
p.parent = s;
s.right = p;
}
else {
TreeNode<K,V> sp = s.parent;
if ((p.parent = sp) != null) {
if (s == sp.left)
sp.left = p;
else
sp.right = p;
}
if ((s.right = pr) != null)
pr.parent = s;
}
p.left = null;
if ((p.right = sr) != null)
sr.parent = p;
if ((s.left = pl) != null)
pl.parent = s;
if ((s.parent = pp) == null)
root = s;
else if (p == pp.left)
pp.left = s;
else
pp.right = s;
if (sr != null)
replacement = sr;
else
replacement = p;
}
else if (pl != null)
replacement = pl;
else if (pr != null)
replacement = pr;
else
replacement = p;
if (replacement != p) {
TreeNode<K,V> pp = replacement.parent = p.parent;
if (pp == null)
root = replacement;
else if (p == pp.left)
pp.left = replacement;
else
pp.right = replacement;
p.left = p.right = p.parent = null;
}
TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);
if (replacement == p) { // detach
TreeNode<K,V> pp = p.parent;
p.parent = null;
if (pp != null) {
if (p == pp.left)
pp.left = null;
else if (p == pp.right)
pp.right = null;
}
}
if (movable)
moveRootToFront(tab, r);
}
其他的HashMap的源码在前面HashSet里面已经写过了。
HashMap的死循环问题
这个链接比较清晰的写了造成死循环的情况
我认为关键的情况就是在并发的过程中造成的不可重复读现象