ConcurrentHashMap-红黑树

一、红黑树基础介绍

1、红黑树是一种平衡的二叉树,通过颜色约束和旋转操作保持树的平衡,确保查找、插入和删除的时间复杂度为O(log n)。

核心规则

  1.  颜色规则:每个节点非红即黑
  2.  根节点规则:根节点必须是黑色
  3.  叶子规则:所有叶子节点(NIL节点)为黑色
  4.  红色节点规则:红色节点的子节点必须为黑色
  5.  黑高规则:从任一节点到所有叶子节点的路径包含相同数量的黑色节点。

二、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;
}

关键步骤

  1. 查找插入位置:根据哈希值和键的比较,确定插入方向(左/右子树)。

  2. 处理哈希冲突:若键不可直接比较,递归搜索子树或通过 tieBreakOrder 决定方向。

  3. 创建新节点:在合适位置插入新节点。

  4. 平衡调整:调用 balanceInsertion 调整颜色和结构,维持红黑树性质。

  5. 根节点维护:确保根节点位于桶的首位,便于后续操作。

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 的右子节点:

  1. 违反规则CD 均为红色。

  2. 平衡调整

    • Case 3:若 C 的叔叔节点 A 是红色,将 AC 变黑,祖父 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; // 返回调整后的根节点
}

左旋操作

  1. 将节点 p 的右子节点 r 提升为父节点。

  2. r 的左子节点 rl 变为 p 的右子节点。

  3. 更新父节点引用,确保树结构的正确性。

 

三、并发控制机制

  • 锁粒度TreeBin 通过内置的 synchronized 锁保护树的修改操作。

  • CAS 操作:在树的根节点维护(moveRootToFront)时使用 CAS 更新桶的引用。

  • 状态检查:在插入/删除前检查根节点是否变化,避免并发修改导致的数据不一致。

四、总结

ConcurrentHashMap 中的红黑树通过精细的平衡调整(旋转、颜色翻转)和并发控制(锁、CAS),在保证线程安全的同时,提供了高效的查找性能。理解其源码需结合树的结构特性与并发编程技巧,是深入掌握 Java 并发集合类的关键。

 

### ConcurrentHashMap链表换为红黑树的机制 在 `ConcurrentHashMap` 的实现中,当哈希桶中的链表长度达到一定阈值时,会将该链表换为红黑树。这一操作旨在优化数据结构以提高查找效率。 具体来说,在执行 `put` 操作期间,一旦检测到某个哈希桶内的链表元素数量超过设定的阈值 `TREEIFY_THRESHOLD`(默认值为8),就会触发链表红黑树变[^1]: ```java if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); ``` 此逻辑确保了即使在高并发环境下频繁插入新键值对的情况下,也能维持较好的访问性能。 ### 设置阈值的原因分析 选择 8 作为从链表变为红黑树的关键点并非随意决定,而是基于多方面考量的结果。一方面,随着链表的增长,其线性遍历的时间复杂度 O(n) 显著影响整体性能;另一方面,过早地采用更为复杂的红黑树结构也会带来额外的空间消耗以及维护成本。因此,在两者间找到合适的折衷方案至关重要。 研究表明,当链表长度接近或超过 8 时,继续沿用链表形式所带来的边际效益逐渐减少甚至变为负数——即每次新增加一个节点所付出的成本远高于获得的好处。此时切换至时间复杂度更低、查询速度更快但相对占用更多资源的数据结构成为合理的选择[^3]。 值得注意的是,虽然理论上任何大于零整数值都可以被指定为此处提到的临界大小参数,但在实际应用中通常建议保持默认配置不变除非有充分理由相信特定场景下其他取值能够带来更多优势。 ### 相关问题
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值