一、红黑树基础介绍
1、红黑树是一种平衡的二叉树,通过颜色约束和旋转操作保持树的平衡,确保查找、插入和删除的时间复杂度为O(log n)。
核心规则
- 颜色规则:每个节点非红即黑
- 根节点规则:根节点必须是黑色
- 叶子规则:所有叶子节点(NIL节点)为黑色
- 红色节点规则:红色节点的子节点必须为黑色
- 黑高规则:从任一节点到所有叶子节点的路径包含相同数量的黑色节点。
二、ConcurrentHashMap中的红黑树
在 ConcurrentHashMap 中,当链表长度超过阈值(默认 8)时,且数组长度大于64,链表转换为红黑树(TreeBin);当节点数减少到阈值以下(默认 6),树退化为链表。
关键类:
TreeBin:封装红黑树结构,持有根节点和锁,负责树的并发控制。
TreeNode:红黑树节点,包含父节点、左右子节点、颜色等属性。
三、源码分析
3.1、插入操作
final TreeNode<K,V> putTreeVal(int h, K k, V v) { Class<?> kc = null; boolean searched = false; TreeNode<K,V> root = (parent != null) ? root() : this; // 获取根节点 for (TreeNode<K,V> p = root;;) { // 从根节点开始查找插入位置 int dir, ph; K pk; if ((ph = p.hash) > h) // 当前节点哈希值更大,向左子树查找 dir = -1; else if (ph < h) // 向右子树查找 dir = 1; else if ((pk = p.key) == k || (k != null && k.equals(pk))) // 键已存在,直接返回 return p; else if ((kc == null && (kc = comparableClassFor(k)) == null) || // 检查是否可比较 (dir = compareComparables(kc, k, pk)) == 0) { // 不可比较,递归搜索左右子树 if (!searched) { TreeNode<K,V> q, ch; searched = true; if (((ch = p.left) != null && (q = ch.findTreeNode(h, k, kc)) != null) || ((ch = p.right) != null && (q = ch.findTreeNode(h, k, kc)) != null)) return q; } dir = tieBreakOrder(k, pk); // 通过系统哈希码决定方向 } TreeNode<K,V> xp = p; if ((p = (dir <= 0) ? p.left : p.right) == null) { // 找到插入位置 TreeNode<K,V> x = map.newTreeNode(h, k, v, xp); // 创建新节点 if (dir <= 0) xp.left = x; // 插入左子树 else xp.right = x; // 插入右子树 root = balanceInsertion(root, x); // 平衡调整 break; } } moveRootToFront(tab, root); // 确保根节点在桶的首位(便于遍历) return null; }
关键步骤
查找插入位置:根据哈希值和键的比较,确定插入方向(左/右子树)。
处理哈希冲突:若键不可直接比较,递归搜索子树或通过
tieBreakOrder
决定方向。创建新节点:在合适位置插入新节点。
平衡调整:调用
balanceInsertion
调整颜色和结构,维持红黑树性质。根节点维护:确保根节点位于桶的首位,便于后续操作。
3.2 平衡插入
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root, TreeNode<K,V> x) { x.red = true; // 新插入节点初始为红色 for (TreeNode<K,V> xp, xpp, xppl, xppr;;) { if ((xp = x.parent) == null) { // Case 1: x是根节点 x.red = false; // 根节点必须为黑色 return x; } else if (!xp.red || (xpp = xp.parent) == null) // Case 2: 父节点是黑色或根节点 return root; // 无需调整 // 父节点是红色,且存在祖父节点 if (xp == (xppl = xpp.left)) { // 父节点是左子节点 if ((xppr = xpp.right) != null && xppr.red) { // Case 3: 叔叔节点是红色 xppr.red = false; // 叔叔变黑 xp.red = false; // 父节点变黑 xpp.red = true; // 祖父变红 x = xpp; // 将祖父作为新节点继续调整 } else { // Case 4: 叔叔是黑色或不存在 if (x == xp.right) { // Case 4a: x是右子节点 root = rotateLeft(root, x = xp); // 左旋父节点 xpp = (xp = x.parent) == null ? null : xp.parent; } if (xp != null) { // Case 4b: x是左子节点 xp.red = false; // 父节点变黑 if (xpp != null) { xpp.red = true; // 祖父变红 root = rotateRight(root, xpp); // 右旋祖父节点 } } } } else { // 父节点是右子节点(对称操作) // ... 类似上述逻辑,方向相反 } } }
平衡调整场景:
Case 1:新节点是根节点,直接变黑。
Case 2:父节点是黑色,无需调整。
Case 3:父节点和叔叔节点均为红色,将父叔变黑,祖父变红,递归调整祖父。
Case 4:父节点红,叔叔黑,通过旋转和颜色翻转恢复平衡。
示例:插入与平衡调整
假设初始树结构如下(括号内为颜色):
复制
B(黑) / \ A(红) C(红)插入新节点
D(红)
到C
的右子节点:
违反规则:
C
和D
均为红色。平衡调整:
Case 3:若
C
的叔叔节点A
是红色,将A
和C
变黑,祖父B
变红。Case 4:若
A
是黑色,对C
左旋,再对B
右旋并调整颜色。
3.3 左旋
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root, TreeNode<K,V> p) { TreeNode<K,V> r, pp, rl; if (p != null && (r = p.right) != null) { // 确保p和右子节点存在 if ((rl = p.right = r.left) != null) // p的右子节点指向r的左子节点 rl.parent = p; // 更新rl的父节点 if ((pp = r.parent = p.parent) == null) // r的父节点指向p的父节点 (root = r).red = false; // 若p是根节点,r成为新根并变黑 else if (pp.left == p) // p是左子节点 pp.left = r; // 更新父节点的左子节点为r else pp.right = r; // 更新父节点的右子节点为r r.left = p; // r的左子节点指向p p.parent = r; // p的父节点指向r } return root; // 返回调整后的根节点 }
左旋操作:
将节点
p
的右子节点r
提升为父节点。将
r
的左子节点rl
变为p
的右子节点。更新父节点引用,确保树结构的正确性。
三、并发控制机制
-
锁粒度:
TreeBin
通过内置的synchronized
锁保护树的修改操作。 -
CAS 操作:在树的根节点维护(
moveRootToFront
)时使用 CAS 更新桶的引用。 -
状态检查:在插入/删除前检查根节点是否变化,避免并发修改导致的数据不一致。
四、总结
ConcurrentHashMap
中的红黑树通过精细的平衡调整(旋转、颜色翻转)和并发控制(锁、CAS),在保证线程安全的同时,提供了高效的查找性能。理解其源码需结合树的结构特性与并发编程技巧,是深入掌握 Java 并发集合类的关键。