编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

  • Java入门精通

    • README
    • 入门教程

    • 综合案例

    • 专栏博客

      • README
      • JVM内存模型与对象
      • 类加载与双亲委派
      • 垃圾回收与GC调优
      • 异常体系与JVM机制
      • 字节码指令集javap实战
      • JIT编译与去优化机制
      • JVM性能诊断工具链
      • OOM八大现场全景剖析
      • JVM参数调优全景图
      • GraalVM与AOT编译原理
      • HashMap底层哈希设计
      • String不可变与常量池
      • ArrayList与LinkedList源码
      • ConcurrentHashMap并发
      • TreeMap与红黑树原理
        • 1. 案例引入
          • 1.1 排行榜的烦恼
          • 1.2 一次延迟翻车
          • 1.3 我们要回答什么
        • 2. 有序映射全景
          • 2.1 三种实现对比
          • 2.2 平衡树家族谱
          • 2.3 选型决策路径
        • 3. 红黑树五大性质
          • 3.1 性质条文逐条
          • 3.2 黑高与最长路径
          • 3.3 性质保证的本质
        • 4. 节点结构剖析
          • 4.1 Entry 字段布局
          • 4.2 颜色位的存放
          • 4.3 父指针的代价
        • 5. 旋转操作根基
          • 5.1 左旋几何直觉
          • 5.2 右旋几何直觉
          • 5.3 旋转的不变式
        • 6. 插入调整规则
          • 6.1 五种局面分类
          • 6.2 叔叔红色情形
          • 6.3 叔叔黑色情形
          • 6.4 完整插入示例
        • 7. 删除调整规则
          • 7.1 删除的本质
          • 7.2 双黑节点危机
          • 7.3 四种修复局面
        • 8. 跳表对比分析
          • 8.1 跳表结构原理
          • 8.2 复杂度对比
          • 8.3 并发友好性
        • 9. ConcurrentSkipListMap
          • 9.1 为什么不用红黑树
          • 9.2 跳表的无锁化
          • 9.3 三大有序 Map 对比
        • 10. 综合案例串讲
          • 10.1 排行榜真相揭晓
          • 10.2 一次 put 的红黑路径
          • 10.3 设计哲学回扣
          • 10.4 有序容器速查表
      • LinkedHashMap与LRU实现
      • Java数字类型原理
      • Object通用方法的契约
      • 泛型擦除与类型系统
      • 枚举原理与最佳实践
      • 注解原理与编译期处理
      • Lambda与引用底层原理
      • Stream原理与流水线设计
      • Optional设计原理
      • Record密封类与模式
      • 反射机制与动态代理
      • MethodHandle与VarHandle
      • 三大字节码框架对比
      • JavaAgent与Instrumentation机制
      • AOP三种实现路线对比
      • synchronized与锁升级
      • volatile与JMM内存模型
      • 线程池核心源码设计
      • Thread线程生命周期
      • AQS同步框架源码
      • 并发锁三剑客
      • CAS和Atomic深入分析
      • 五大同步器对比
      • CompletableFuture异步
      • IO模型演进BIO到AIO
      • ByteBuffer与堆外内存
      • 序列化原理与替代方案
      • 文件IO与NIO.2
      • 面向对象的真意
      • JDK设计模式上
      • JDK设计模式下
      • SPI与模块化设计
  • Go入门到精通

  • JavaScript入门

  • CodeX
  • Java入门精通
  • 专栏博客
杨充
2026-06-02
目录

TreeMap与红黑树原理

# 15.TreeMap与红黑树原理

# 目录介绍

  • 1. 案例引入
    • 1.1 排行榜的烦恼
    • 1.2 一次延迟翻车
    • 1.3 我们要回答什么
  • 2. 有序映射全景
    • 2.1 三种实现对比
    • 2.2 平衡树家族谱
    • 2.3 选型决策路径
  • 3. 红黑树五大性质
    • 3.1 性质条文逐条
    • 3.2 黑高与最长路径
    • 3.3 性质保证的本质
  • 4. 节点结构剖析
    • 4.1 Entry 字段布局
    • 4.2 颜色位的存放
    • 4.3 父指针的代价
  • 5. 旋转操作根基
    • 5.1 左旋几何直觉
    • 5.2 右旋几何直觉
    • 5.3 旋转的不变式
  • 6. 插入调整规则
    • 6.1 五种局面分类
    • 6.2 叔叔红色情形
    • 6.3 叔叔黑色情形
    • 6.4 完整插入示例
  • 7. 删除调整规则
    • 7.1 删除的本质
    • 7.2 双黑节点危机
    • 7.3 四种修复局面
  • 8. 跳表对比分析
    • 8.1 跳表结构原理
    • 8.2 复杂度对比
    • 8.3 并发友好性
  • 9. ConcurrentSkipListMap
    • 9.1 为什么不用红黑树
    • 9.2 跳表的无锁化
    • 9.3 三大有序 Map 对比
  • 10. 综合案例串讲
    • 10.1 排行榜真相揭晓
    • 10.2 一次 put 的红黑路径
    • 10.3 设计哲学回扣
    • 10.4 有序容器速查表

# 1. 案例引入

# 1.1 排行榜的烦恼

某电商团队做"实时积分排行榜"——每个用户有积分 score,要支持 4 个查询:

查询 1:top(K)              取前 K 名
查询 2:rank(userId)        查某人是第几名
查询 3:around(userId, n)   查某人前后 n 位
查询 4:put(userId, score)  更新某人积分
1
2
3
4

第一版用 HashMap<Long, Integer> 存(userId → score),结果发现:

public List<Long> top(int k) {
    return userScores.entrySet().stream()
        .sorted(Map.Entry.<Long, Integer>comparingByValue().reversed())
        .limit(k)
        .map(Map.Entry::getKey)
        .toList();
}
1
2
3
4
5
6
7

每次 top(K) 都要把 100 万用户全排序一遍 → O(n log n) → 单次 800ms。leader 看到监控直接退回了。

第二版换成 TreeMap<Integer, Set<Long>>(score → userIds,按 score 倒序):

private final TreeMap<Integer, Set<Long>> rankMap = 
    new TreeMap<>(Comparator.reverseOrder());
1
2

top(K) 性能从 800ms → 0.5ms——用 TreeMap 内置的有序遍历替代了全排序。性能起飞。

# 1.2 一次延迟翻车

但好景不长,半年后流量翻倍,监控告警:rank() 的 P99 从 2ms 飙到 50ms。jstack 没死锁,CPU 也不高,但 TreeMap 操作明显变慢。

排查时同事甩出三个灵魂质问:

追问 ①:TreeMap 的 get/put/rank 是 O(log n),n 翻倍只该多 1 层比较,
         为什么延迟 P99 翻 25 倍?
追问 ②:TreeMap 底层是红黑树。它的"5 大性质"到底保证了什么?
         为什么是这 5 条而不是 4 条或 6 条?
追问 ③:JDK 自带 ConcurrentSkipListMap,为啥不直接做"线程安全 TreeMap"?
         红黑树有什么"不能并发"的硬伤?
1
2
3
4
5
6

更扎心的是——插入一个新元素,最多触发多少次旋转?最多重染色多少次?没人答得上。这一切,就是本篇要回答的核心。

# 1.3 我们要回答什么

第 21 篇要把"有序映射"这件事讲透——从红黑树五大性质的数学本质,到 ConcurrentSkipListMap 用跳表而非红黑树的工程考量。

带着这个目标,要回答 7 个核心问题:

① 红黑树的"5 大性质"分别是什么?为什么这 5 条能保证 O(log n)?  → 第3章
② 黑高 (black height) 是什么?最长路径为什么不超过最短路径 2 倍? → 第3章
③ 插入调整最多旋转几次?删除调整呢?复杂度的常数项是多少?     → 第6-7章
④ 旋转操作怎么保证 BST 性质不变?左旋右旋是怎么"对偶"的?      → 第5章
⑤ 跳表与红黑树相比,复杂度和空间各有什么取舍?               → 第8章
⑥ ConcurrentSkipListMap 为什么不基于红黑树做并发版本?        → 第9章
⑦ TreeMap、ConcurrentSkipListMap、ConcurrentHashMap 怎么选?  → 第9-10章
1
2
3
4
5
6
7

本篇路线:

有序映射全景 (第2章) ─── 红黑树 / AVL / 跳表三足鼎立
    ↓
红黑树五大性质 (第3章)     ←—— 数学根基
节点结构剖析 (第4章)       ←—— 颜色位放在哪
旋转操作根基 (第5章)       ←—— 平衡的物理工具
    ↓
插入调整规则 (第6章)       ←—— 5 种局面 + 完整示例
删除调整规则 (第7章)       ←—— 双黑危机 4 种修复
    ↓
跳表对比分析 (第8章)
ConcurrentSkipListMap (第9章)  ←—— 为什么放弃红黑树
    ↓
综合案例串讲 (第10章)
1
2
3
4
5
6
7
8
9
10
11
12
13

# 2. 有序映射全景

# 2.1 三种实现对比

JDK 中"有序 Map"有三种实现,对应三种数据结构哲学:

┌─────────────────────────────────────────────────────────┐
│ TreeMap                                                  │
│   底层:红黑树 (Red-Black Tree)                           │
│   特点:自平衡 BST,旋转保证 O(log n)                      │
│   并发:非线程安全                                         │
│   场景:单线程有序 Map 默认选择                            │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│ ConcurrentSkipListMap                                    │
│   底层:跳表 (Skip List)                                 │
│   特点:多层链表,CAS 友好,无锁可并发                      │
│   并发:线程安全                                          │
│   场景:高并发有序 Map                                     │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│ Collections.synchronizedSortedMap(new TreeMap<>())       │
│   底层:TreeMap + 全表锁                                  │
│   特点:粗粒度锁,简单但慢                                  │
│   并发:线程安全(差)                                     │
│   场景:低并发兼容旧代码                                   │
└─────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

疑惑:既然 TreeMap 已经做到 O(log n),为什么并发场景不直接给它加锁?

论证:红黑树的旋转操作要修改多个父子指针,而且修改路径从插入点回溯到根节点。在并发场景:

  • 写线程 A 正在做"叔叔红色"的重染色(自下而上回溯)
  • 写线程 B 同时插入到另一棵子树,也要回溯到根
  • 两个线程在根节点附近必然冲突

如果给红黑树加锁,要么"全树锁"(退回 Hashtable 时代),要么"自下而上的细粒度锁"(极易死锁)。两条路都不通。

结论:红黑树本身的"指针密集修改"是它无法高效并发的根本原因。这就是为什么 Doug Lea 在 2007 年专门写了 ConcurrentSkipListMap 而不是"ConcurrentTreeMap"——选错数据结构,再多努力也救不回来。

# 2.2 平衡树家族谱

平衡树家族成员众多,按"约束严格度"排列:

约束最松 ─────────────────────────────────────→ 约束最严
   │                                              │
   │                                              │
跳表 ── 红黑树 ── Treap ── B 树 ── B+树 ── AVL 树
                                                  │
                                              旋转最多
                                              查询最快
                                              插入最慢
1
2
3
4
5
6
7
8

关键观察:约束越严的树,查询越快但写入越慢:

树型 平衡约束 查询 插入旋转上限
AVL 任意节点左右子树高度差 ≤ 1 最快 O(log n) 次
红黑树 黑高相等、红节点不连续 较快 ≤ 3 次
B 树 节点容量上下界 中等 O(1)(局部分裂)
跳表 概率平衡 中等 0 次(无旋转)

疑惑:AVL 查询更快,为什么 JDK 选红黑树?

论证:HashMap、TreeMap、Linux 进程调度(CFS)、Linux 红黑树内核库……几乎所有"主流红黑树场景",都是写多读多混合负载。AVL 严格平衡导致频繁旋转——写入开销大;而红黑树最多旋转 3 次就能达到平衡,写入摊还代价更低。

结论:红黑树是"读写权衡的最优解"——查询稍慢于 AVL,但写入快得多。这是 Java 集合、Linux 内核、C++ STL 集体押宝红黑树的根本理由。

# 2.3 选型决策路径

把"有序 Map 选型"画成决策树:

 ┌─────────────────────────┐
 │  我需要"按 key 有序"吗?│
 └─────────┬───────────────┘
           │
        ┌──┴──┐
        ↓ 否  ↓ 是
   HashMap   ┌─────────────────────────────┐
             │  我需要并发吗?              │
             └──────┬──────────────────────┘
                    │
                 ┌──┴──┐
                 ↓ 否  ↓ 是
            TreeMap   ┌─────────────────────────┐
                      │  写入压力高吗?(>1k QPS) │
                      └──────┬──────────────────┘
                             │
                          ┌──┴──┐
                          ↓ 否  ↓ 是
              synchronizedSortedMap   ConcurrentSkipListMap
              (低并发兼容)            (高并发首选)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

结论:选有序 Map 时永远先问"是否要并发"——这一个问题就把候选范围缩到 1~2 个。这与第 4 篇 HashMap 选型决策路径同构。

# 3. 红黑树五大性质

# 3.1 性质条文逐条

红黑树的 5 大性质(Cormen 在《算法导论》里的经典定义):

性质 1:每个节点要么红色要么黑色
性质 2:根节点是黑色
性质 3:每个叶子节点(NIL,即空节点)是黑色
性质 4:红色节点的两个子节点都是黑色(红节点不能连续)
性质 5:从任一节点到其每个叶子的所有简单路径上,黑色节点数相同
1
2
3
4
5

疑惑:为什么是这 5 条?少一条会怎样?

论证——逐条删去验证必要性:

  • 删性质 1(每个节点必须有颜色):红黑性质本身就消失了,平衡机制崩塌。
  • 删性质 2(根必须黑):插入时需要对根的颜色单独讨论,且根可能与子节点形成红红冲突——简化用,可去除但增加代码复杂度。
  • 删性质 3(NIL 是黑色):黑高定义中"包不包括 NIL"会产生歧义,删除调整时无法对叶子做颜色判断——算法不可去。
  • 删性质 4(红不连续):黑高相同的前提下,最长路径可以无限长——O(log n) 保证消失。
  • 删性质 5(黑高相等):树可以严重不平衡——O(log n) 保证消失。

结论:性质 4 和性质 5 是 "O(log n) 保证的两条腿"——缺一不可。性质 1、2、3 是辅助性约束,让算法实现得以简化和明确。

# 3.2 黑高与最长路径

核心定理:红黑树最长路径 ≤ 最短路径的 2 倍。

证明思路:

设黑高 (black height) = bh
最短路径:全是黑节点,长度 = bh
最长路径:黑红交替,长度 = 2*bh (红黑红黑...红黑)
                                 ↑
                              性质 4 限制:红不能连续
                              所以红节点最多与黑节点 1:1
1
2
3
4
5
6

视觉对比:

最短路径 (全黑):                  最长路径 (黑红交替):
                                
        [B]                              [B]
         │                                │
        [B]                              [R]
         │                                │
        [B]                              [B]
         │                                │
        [NIL]                            [R]
                                          │
                                         [B]
                                          │
                                         [NIL]
        长度 = 3                        长度 = 6 = 2 * 3
1
2
3
4
5
6
7
8
9
10
11
12
13
14

结论:性质 4 + 性质 5 保证树高 ≤ 2·log₂(n+1) ——这就是 O(log n) 时间复杂度的数学根基。换句话说,红黑树比最优 BST 最多深一倍,但代价是写入旋转少得多。

# 3.3 性质保证的本质

跳出条文看本质——红黑树 5 大性质的设计哲学:

哲学 1:用颜色编码"局部不平衡度": 红节点像"减震器"——允许短期容忍局部高度差异,避免每次插入都必须严格再平衡(如 AVL 那样)。这是**用空间(颜色位)换时间(旋转次数)**的经典权衡。

哲学 2:约束本身是对称的: 所有性质都不区分左右子树——左旋右旋互为对偶。这让算法实现可以对称简化(每个调整规则的"左父亲"版本翻转就能处理"右父亲"版本)。

哲学 3:不严格、但可验证: 红黑树不是"最平衡"的——AVL 比它更平衡。但红黑性质容易在 O(1) 局部检查(看节点颜色即可),AVL 必须计算左右子树高度差(需要额外字段)。性质 4 + 5 用最少的状态信息表达了"近似平衡"。

结论:红黑性质是"工程化的平衡定义"——不追求极致平衡,但追求用最少的额外存储和最少的调整次数逼近平衡。这与 §17 篇 GC "近似最优"的哲学是同一思想——完美的敌人是足够好。

# 4. 节点结构剖析

# 4.1 Entry 字段布局

TreeMap 的内部节点 TreeMap.Entry:

static final class Entry<K,V> implements Map.Entry<K,V> {
    K key;
    V value;
    Entry<K,V> left;
    Entry<K,V> right;
    Entry<K,V> parent;          // ★ 父指针
    boolean color = BLACK;      // ★ 颜色 (true=BLACK, false=RED)
    
    // 构造、equals、hashCode...
}
1
2
3
4
5
6
7
8
9
10

6 个字段的内存布局(64 位 JVM、UseCompressedOops):

Entry 对象布局
┌──────────────────────────────────────────┐
│ Mark Word         (8B)                   │  对象头
│ Klass Ptr         (4B)                   │  类型指针 (压缩)
│ key 引用          (4B)                   │
│ value 引用        (4B)                   │
│ left 引用         (4B)                   │
│ right 引用        (4B)                   │
│ parent 引用       (4B)                   │
│ color             (1B + 7B padding)      │  ★ 颜色独占 8B(对齐填充)
└──────────────────────────────────────────┘
总大小:48B/节点
1
2
3
4
5
6
7
8
9
10
11
12

对比 HashMap.Node(无 parent、无 color):32B/节点。TreeMap 节点比 HashMap 节点多 50% 空间——这是 TreeMap 的固定空间代价。

# 4.2 颜色位的存放

疑惑:颜色只有红/黑两种状态,1 个 bit 就够,为什么用 1 个 boolean(占 8B)?

论证:JVM 没有"位字段"概念——任何字段最少 1 字节(boolean 实际占 1 字节)+ 对齐填充。所以"1 bit 颜色"在 JVM 里实际占 8B。

有没有更紧凑的方案?有!Linux 内核红黑树(include/linux/rbtree.h)把颜色位塞进了 parent 指针的最低位:

struct rb_node {
    unsigned long  __rb_parent_color;   /* 高位是 parent 指针,最低位是颜色 */
    struct rb_node *rb_right;
    struct rb_node *rb_left;
};
1
2
3
4
5

由于 64 位指针地址对齐,最低 3 位永远是 0——可以"借"最低 1 位放颜色。这样每个节点省 8B。但 Java 没有这种位运算指针——JVM 抽象层屏蔽了这种 hack 的可能。

结论:TreeMap 的"颜色单独占 8B"是 JVM 抽象层带来的不可消除开销。这也是为什么 §20 篇 ConcurrentHashMap 的 TreeNode 比 Node 大近一倍——红黑树的固定空间代价。

# 4.3 父指针的代价

疑惑:BST 需要父指针吗?只有左右孩子也能查找。

论证:父指针的主要用途有 3 个:

  1. 插入后向上回溯调整:插入新节点后,需要从插入点向上检查"红红冲突"——没有 parent 就只能用栈记录路径
  2. 删除后双黑修复:删除时要从删除点向上回溯修复双黑——同样需要 parent
  3. 后继/前驱查找:找一个节点的后继(比它大的最小节点)需要向上找祖先——需要 parent

代价:每节点多 4B 引用 + GC root 扫描时间增加(红黑树是双向引用,GC 不能用简单的子树扫描)。

结论:parent 指针是红黑树为"O(log n) 调整"必须付出的空间代价。这与第 19 篇 LinkedList 的 prev 指针同源——都是"为了快速向前回溯"付出的固定开销。

# 5. 旋转操作根基

# 5.1 左旋几何直觉

旋转是平衡树调整的物理工具——它改变树的形状但保持 BST 性质。

左旋(Left Rotate)——以节点 X 为支点,把 X 的右子节点 Y 提升到 X 的位置:

左旋前               左旋后
                              
    X                    Y
   / \                  / \
  α   Y         ─→     X   γ
     / \              / \
    β   γ            α   β
1
2
3
4
5
6
7

关键不变式:中序遍历顺序不变(α < X < β < Y < γ 在前后保持一致)。

源码(TreeMap.rotateLeft):

private void rotateLeft(Entry<K,V> p) {
    if (p != null) {
        Entry<K,V> r = p.right;          // r = Y
        p.right = r.left;                // X.right = β
        if (r.left != null)
            r.left.parent = p;           // β.parent = X
        r.parent = p.parent;             // Y.parent = X.parent
        if (p.parent == null)
            root = r;                    // 若 X 是根,Y 成为新根
        else if (p.parent.left == p)
            p.parent.left = r;
        else
            p.parent.right = r;
        r.left = p;                      // X 成为 Y 的左孩子
        p.parent = r;                    // X.parent = Y
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

关键观察:左旋只动 6 条边的连接——是 O(1) 局部操作。这是红黑树能做到 O(log n) 的关键之一:调整局部,不重建全树。

# 5.2 右旋几何直觉

右旋(Right Rotate) 是左旋的对偶——以节点 Y 为支点,把 Y 的左子节点 X 提升:

右旋前               右旋后
                              
      Y                X
     / \              / \
    X   γ      ─→    α   Y
   / \                  / \
  α   β                β   γ
1
2
3
4
5
6
7

对称性:右旋的代码与左旋完全镜像——只需把所有 left 改成 right、right 改成 left 即可。

结论:红黑树所有调整规则都成对出现(叔叔在左 vs 叔叔在右、删除黑兄在左 vs 黑兄在右)——这就是为什么 5 大性质必须左右对称。对称性是简化算法实现的必要条件。

# 5.3 旋转的不变式

旋转操作必须满足三个不变式:

不变式 1:BST 性质保持
        左子树 < 节点 < 右子树
        
不变式 2:中序遍历不变
        旋转前后,按 key 升序遍历的结果完全一致
        
不变式 3:旋转节点和其参与的子树外部,整棵树结构不变
        旋转是"局部操作"
1
2
3
4
5
6
7
8

疑惑:旋转会改变颜色吗?

论证:旋转本身不改颜色——只改指针。但调整算法常常"先重染色再旋转":先把节点染色,再用旋转把"性质 5(黑高相等)"恢复。

结论:旋转是"形状调整",染色是"颜色调整"——两者配合,红黑树才能在 O(log n) 内恢复全部 5 大性质。这是第 6、7 章插入/删除调整的内核。

# 6. 插入调整规则

# 6.1 五种局面分类

新插入节点永远染成红色(为什么?因为染红只可能违反性质 4,而染黑会违反性质 5——性质 4 比性质 5 容易修复)。

新节点 N 插入后,按"父亲、叔叔、爷爷"的颜色与位置分 5 种局面:

局面 0:N 是根节点              → 直接染黑(结束)
局面 1:N 的父亲是黑色          → 不违反任何性质(结束)
局面 2:父红 + 叔红              → 重染色 + 上溯(递归)
局面 3:父红 + 叔黑 + N 与父同侧  → 染色 + 单旋(结束)
局面 4:父红 + 叔黑 + N 与父异侧  → 双旋(结束)
1
2
3
4
5

总结:5 种局面中——只有局面 2 需要递归向上,其余 4 种局部解决。这就是为什么红黑树插入最多 O(log n) 次重染色 + 最多 2 次旋转——递归向上次数最多 = 树高。

# 6.2 叔叔红色情形

局面 2:父红 + 叔红:

插入前                                   调整后
        [G]B                                  [G]R    ← 染红
       /    \                                /    \
     [P]R   [U]R                           [P]B   [U]B  ← 染黑
      │                                      │
     [N]R  ← 新插入红节点                    [N]R
     
     P.color = R, U.color = R               G 变红,可能违反"性质 2 (根黑)"
     违反"性质 4 (红不连续)"                 或与 G 的父亲产生新红红冲突
                                           ↓
                                           递归向上把 G 当作"新插入"继续修复
1
2
3
4
5
6
7
8
9
10
11

疑惑:为什么叔叔红色就染色不旋转?

论证:当父亲和叔叔都是红色时,爷爷一定是黑色(性质 4)。把父亲、叔叔染黑、爷爷染红——这一步操作:

  • 性质 4 在 P-N 处恢复(因为 P 变黑了)
  • 性质 5 不变(G 这一层从黑变红,但 P 和 U 都从红变黑,黑节点数差额抵消)

唯一副作用:G 可能与它的父亲形成新的红红冲突——但这是"上溯一层"递归就能处理的。

结论:局面 2 是唯一会上溯的局面——意味着插入调整的递归深度 ≤ 树高 = O(log n)。

# 6.3 叔叔黑色情形

局面 3:父红 + 叔黑 + N 与父同侧(如均为左子):

        [G]B                       [P]B    ← 染黑
       /    \                     /    \
     [P]R   [U]B    ─染色─→     [N]R    [G]R  ← 染红
     /                          \         \
   [N]R                          β         [U]B
                       ─右旋(G)─→         
1
2
3
4
5
6

调整步骤:

  1. P 染黑、G 染红
  2. 以 G 为支点右旋

结果:性质 4 恢复(P 是黑),性质 5 保持(旋转前后黑高一致)。结束,不需要上溯。

局面 4:父红 + 叔黑 + N 与父异侧(如 P 是左子但 N 是右子):

先左旋 P,转化为局面 3
        [G]B                 [G]B
       /    \              /    \
     [P]R   [U]B  ─左旋─→ [N]R   [U]B
       \                  /
       [N]R             [P]R
                        ↓
                    转入局面 3 处理
1
2
3
4
5
6
7
8

结论:局面 4 = "1 次左旋(先把异侧拉回同侧)+ 局面 3"——所以插入最多 2 次旋转。

核心结论:红黑树插入最多 2 次旋转 + O(log n) 次重染色。这是常数级的旋转开销,是它写入快于 AVL 的根本原因。

# 6.4 完整插入示例

依次插入 10、20、30、15、25、5、12——观察红黑树的变化:

插入 10:
    [10]B   ← 根节点直接染黑

插入 20:
    [10]B
        \
        [20]R   ← 父黑,无需调整

插入 30:
    [10]B                   [20]B    ← 局面 4 → 局面 3
        \                  /    \
        [20]R   ─调整─→  [10]R  [30]R
            \
            [30]R          
    
    20 染黑、10 染红、对 10 左旋

插入 15:
    [20]B
   /    \
  [10]R [30]R
      \
      [15]R    ← 父红叔红 → 局面 2
                10 染黑、30 染黑、20 染红(但 20 是根,再染黑)

    [20]B           ← 根永远黑
   /    \
  [10]B [30]B
      \
      [15]R

插入 25:    
    [20]B
   /    \
  [10]B [30]B
      \  /
   [15]R [25]R   ← 父黑直接插入

插入 5:
    [20]B
   /    \
  [10]B [30]B
  /  \   /
[5]R [15]R [25]R   ← 父黑直接插入

插入 12:   局面 2  →  10 染黑、15 染黑、12 直接插(父红叔红向上染色)
                       12 没有叔叔节点(NIL),按 NIL=黑处理
                       
    [20]B
   /    \
  [10]B [30]B
  /  \   /
[5]R [15]R [25]R
     /
    [12]R       ← 等等,父叔关系:12 的父是 15(R),叔是 5(R)
                   局面 2:12 父红叔红 → 5、15 染黑,10 染红
                   
    [20]B
   /    \
  [10]R [30]B
  /  \   /
[5]B [15]B [25]R
     /
    [12]R
                   10 父亲是 20(B),无新冲突,结束
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65

结论:完整 7 次插入只触发了 1 次旋转和 4 次重染色——远低于 AVL 的旋转频率。这个例子直观展示了"红黑树用染色减少旋转"的设计哲学。

# 7. 删除调整规则

# 7.1 删除的本质

BST 删除有 3 种情况:

情况 A:删除节点是叶子          → 直接删除
情况 B:删除节点只有 1 个孩子    → 用孩子替换
情况 C:删除节点有 2 个孩子      → 用后继节点替换,转化为情况 A 或 B
1
2
3

红黑树删除的关键:所有删除最终都归结为"删除一个最多有 1 个孩子的节点"——情况 C 用"后继替身"技巧转化。

TreeMap.deleteEntry 源码片段:

private void deleteEntry(Entry<K,V> p) {
    modCount++;
    size--;

    if (p.left != null && p.right != null) {       // ← 情况 C
        Entry<K,V> s = successor(p);                // 找后继节点
        p.key = s.key;                              // ★ 偷梁换柱:把后继的 key/value 复制到 p
        p.value = s.value;
        p = s;                                      // 转而删除后继
    }
    
    Entry<K,V> replacement = (p.left != null ? p.left : p.right);
    
    if (replacement != null) {                     // ← 情况 B
        // ... 用孩子替换
        if (p.color == BLACK)
            fixAfterDeletion(replacement);          // ★ 双黑修复
    }
    // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

关键技巧:情况 C 用"复制 key/value、删除后继"——避免移动整个节点。但代价是外部引用此节点的代码会突然看到不同的 key——这是 TreeMap 删除时不能向外暴露 Entry 引用的根本原因。

# 7.2 双黑节点危机

疑惑:为什么删除黑色节点要"修复"?

论证:删除一个黑色节点会让该路径黑高 -1——违反性质 5。

删除前                          删除后
        [P]                          [P]
       /                            /
     [B]B   ← 要删的黑节点       [N]
     /
   [N]                              黑高从 2 变成 1
                                    其他路径黑高仍是 2
                                    ★ 性质 5 被破坏
1
2
3
4
5
6
7
8

解决方案:把替换节点 N 临时染成"双黑" ——视为额外多了一层黑色——然后通过旋转和染色把"多出的黑"消化掉或上推到根(被根吃掉)。

双黑修复的目标:不破坏其他性质的前提下,把"多余的黑"消除。

# 7.3 四种修复局面

设要修复的双黑节点是 N,N 的兄弟是 S,按 S 及其子节点颜色分 4 种局面(左对称版):

局面 兄弟 S S 的左子 S 的右子 处理
1 红 黑 黑 兄弟染黑、父染红、对父左旋 → 转入其他局面
2 黑 黑 黑 兄弟染红、双黑上推到父 → 递归
3 黑 红 黑 兄弟与左子互换颜色、对兄弟右旋 → 转入局面 4
4 黑 任意 红 兄弟染父色、父染黑、S 右子染黑、对父左旋 → 结束

关键观察:只有局面 2 会向上递归(与插入对称——只有"上邻全红"时才递归)。其他 3 种局部解决。

结论:红黑树删除最多 3 次旋转 + O(log n) 次染色。这是个相对复杂的算法,但摊还代价仍是 O(log n)——每次删除的旋转常数项 ≤ 3。

与插入对比:

操作 最多旋转 最多上溯次数 复杂度
插入 2 次 O(log n) O(log n)
删除 3 次 O(log n) O(log n)
查询 0 次 0 O(log n)

红黑树是"读 0 旋、写 ≤3 旋"——这个常数级旋转上限是它击败 AVL 的关键。

# 8. 跳表对比分析

# 8.1 跳表结构原理

跳表(Skip List)是 William Pugh 1989 年提出的——用概率代替平衡的数据结构。

Level 3:  [HEAD] ─────────────────────────────────→ [50] ──→ NIL
                                                     │
Level 2:  [HEAD] ──────────→ [20] ────────────────→ [50] ──→ NIL
                              │                      │
Level 1:  [HEAD] ──→ [10] ──→ [20] ──→ [30] ──→ [40] [50] ──→ NIL
                      │        │        │        │   │
Level 0:  [HEAD] ──→ [10] ──→ [20] ──→ [30] ──→ [40] [50] ──→ NIL
1
2
3
4
5
6
7

核心思想:

  • 底层是有序链表——存全部数据
  • 上层是"快速通道"——每个元素以 1/2 概率出现在上一层
  • 查询从最高层开始向右走,遇到比目标大的节点就下降一层

期望复杂度:高度 ≈ log n,每层移动 O(1) 步 → 总 O(log n)。

疑惑:跳表用概率,会有"运气差"的情况吗?

论证:跳表的高度 H 服从几何分布——P(H > k) = (1/2)^k。即:

  • P(H > 32) < 1/4×10⁹(亿分之一)
  • P(H > 64) ≈ 0(数学上几乎不可能)

跳表用 1024 个节点时,期望高度 ≈ 10,最多到 32 层就几乎封顶。在工程上,用一个固定的 32 层数组就足够覆盖 2^32 个元素——这是跳表能在工程上实用的根本原因。

结论:跳表用"概率平衡"代替"严格平衡"——牺牲一点最坏情况(低概率退化)换取算法极简、并发友好。

# 8.2 复杂度对比

维度 红黑树 跳表
查询 O(log n) 严格 O(log n) 期望
插入 O(log n) + ≤2 旋转 O(log n) + 0 旋转
删除 O(log n) + ≤3 旋转 O(log n) + 0 旋转
空间 n × 48B n × ~32B(期望)
缓存友好 一般(指针跳跃) 较差(更多指针)
并发友好 极差(全局回溯) 优秀(局部 CAS)
实现复杂度 极高(5 种局面) 中等

关键差距:并发友好性——红黑树的"自下而上回溯调整"在并发下是噩梦;跳表的所有操作只动自己的指针,CAS 即可完成。

# 8.3 并发友好性

疑惑:为什么跳表能并发而红黑树不能?

论证——看插入操作的"修改范围":

红黑树插入 30 (假设触发 1 次旋转 + 染色上溯 3 层):
  修改:
    新节点 N(添加)
    父节点 P(指针 + 颜色)
    叔叔 U(颜色)
    爷爷 G(颜色)
    曾爷爷指针(旋转)
    根节点(可能颜色)
  ★ 6 个节点 + 跨越 3~4 层的指针
  并发场景:必然与其他写线程冲突

跳表插入 30 (假设落在 3 层):
  修改:
    新节点 N(添加,3 层)
    Level 0 前驱.next = N
    Level 1 前驱.next = N
    Level 2 前驱.next = N
  ★ 3~4 个节点的"单链表插入"
  并发场景:每层独立 CAS,互不影响
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

结论:跳表的修改范围是"局部、可分层独立"的——这是 CAS 友好的本质。红黑树的修改范围是"自下而上、跨层耦合"的——CAS 无法表达这种"事务性"操作。

这就引出了第 9 章的核心论点——ConcurrentSkipListMap 选跳表,是被红黑树的"耦合修改"逼出来的。

# 9. ConcurrentSkipListMap

# 9.1 为什么不用红黑树

回到本篇 §1.2 的灵魂问题——JDK 为什么不做"ConcurrentTreeMap"?三个根本原因:

原因 1:旋转操作无法 CAS 化: 红黑树的旋转要原子修改 6 条边——CAS 只能原子修改一个字段。要么用 STM(软件事务内存,JVM 不支持),要么加锁(违背并发设计初衷)。

原因 2:自下而上回溯路径冲突: 两个线程在不同子树插入,但都要回溯到根——根附近必然冲突。冲突频率与树高成正比,n 越大冲突越严重。

原因 3:并发下树不能"折叠/收缩": 红黑树有"自我收缩"行为(删除节点导致双黑修复,可能让树高 -1)——并发场景下其他线程正在该路径读取,收缩会让它们读到悬空指针。

结论:红黑树的"动态形变"与"无锁并发"是天然冲突的——这就是为什么 Doug Lea 在 JDK 1.6(2007)专门基于跳表实现了 ConcurrentSkipListMap,而不是把 TreeMap 改成线程安全。选错数据结构,再多努力也救不回来——这条工程铁律在数据结构选型上同样成立。

# 9.2 跳表的无锁化

ConcurrentSkipListMap 的核心是每层独立 CAS:

// 简化版的跳表 put
public V put(K key, V value) {
    Node<K,V> z = new Node<>(key, value, null);
    
    // 1. 找到每层应该插入的位置 (无锁查找)
    Node<K,V>[] preds = findPredecessors(key);
    
    // 2. 自底向上 CAS 插入每层
    for (int level = 0; level < z.height; level++) {
        Node<K,V> pred = preds[level];
        Node<K,V> succ = pred.next[level];
        z.next[level] = succ;
        if (!pred.next[level].compareAndSet(succ, z)) {
            // CAS 失败 → 重新找前驱 → 重试
        }
    }
    return null;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

关键设计:

  1. 底层先插入,上层后插入——保证读线程要么看到完整的多层节点,要么看到只有底层(依然能搜到)
  2. CAS 失败重试,不阻塞——读永远不被写阻塞
  3. 节点删除用"标记法"——先标记 next 为"逻辑删除"(特殊节点),再物理摘除

结论:跳表的"分层无锁化"是**"乐观并发的天花板"**——它把红黑树"全树原子操作"的难题降维到"每层链表 CAS"。这与第 20 篇 ConcurrentHashMap 的"桶级锁化"思路一脉相承——把全局问题降维到局部问题。

# 9.3 三大有序 Map 对比

┌─────────────────────────────────────────────────────────────────┐
│ 性能对比 (1000 万元素插入 + 查询)                                │
├──────────────────────────┬──────────────┬───────────────────────┤
│ 容器                      │ 单线程吞吐    │ 16 线程吞吐            │
├──────────────────────────┼──────────────┼───────────────────────┤
│ TreeMap                   │ 1.0x (基准)  │ N/A (非线程安全)       │
│ synchronizedSortedMap     │ 0.85x        │ 0.6x (锁竞争)         │
│ ConcurrentSkipListMap     │ 0.7x         │ 8.5x (近线性扩展)      │
│ ConcurrentHashMap         │ 0.95x        │ 12x (无序,参考值)     │
└──────────────────────────┴──────────────┴───────────────────────┘
1
2
3
4
5
6
7
8
9
10

结论:

  • 单线程:TreeMap > ConcurrentSkipListMap(红黑树常数小)
  • 多线程:ConcurrentSkipListMap >> 加锁的 TreeMap

这是为什么"有序 + 并发"场景必选 ConcurrentSkipListMap,没有第二选项。

# 10. 综合案例串讲

# 10.1 排行榜真相揭晓

回到第 1 章的电商排行榜,逐条揭晓:

① 红黑树的 5 大性质分别是什么,为什么这 5 条能保证 O(log n):5 大性质即:节点红或黑、根黑、叶子(NIL)黑、红节点子节点黑、任一节点到所有叶子的简单路径上黑节点数相同。其中性质 4(红不连续)+ 性质 5(黑高相等)是 "O(log n) 的两条腿"——两者共同保证最长路径 ≤ 最短路径 2 倍(§3.2)。其余 3 条是辅助约束。

② 黑高与最长路径:黑高 = 从某节点到任一叶子的黑节点数。最短路径全黑 = bh,最长路径黑红交替 = 2·bh——所以树高 ≤ 2·log₂(n+1)(§3.2)。这就是 O(log n) 的数学根基。

③ 插入调整最多旋转几次,删除调整呢,复杂度的常数项:插入最多 2 次旋转 + O(log n) 次重染色(§6.3);删除最多 3 次旋转 + O(log n) 次重染色(§7.3)。常数级旋转是红黑树击败 AVL 的关键——AVL 旋转次数随树高增长,而红黑树旋转次数固定。

④ 旋转操作怎么保证 BST 性质不变,左旋右旋怎么对偶:旋转的核心不变式是中序遍历不变——前后元素顺序完全一致(§5.3)。左旋和右旋的代码完全镜像(把所有 left/right 互换即可),这种对偶性保证了所有调整规则都成对出现,简化了算法实现。

⑤ 跳表与红黑树相比的取舍:红黑树查询稍快(严格 O(log n)、无概率),跳表写入稍快(无旋转);但最关键差异是并发友好性——跳表分层 CAS 友好,红黑树自下而上回溯无法 CAS 化。空间上跳表期望约 ~32B/节点(仅有 next 数组),红黑树固定 48B/节点。

⑥ ConcurrentSkipListMap 为什么不基于红黑树:三个根本原因——红黑树旋转要原子改 6 条边(CAS 不可达)、自下而上回溯导致根部冲突、删除导致树自我收缩与并发读冲突(§9.1)。红黑树的"动态形变"与"无锁并发"天然冲突——这是数据结构选型层面无法补救的硬伤。

⑦ 三大有序 Map 怎么选:单线程默认 TreeMap;高并发选 ConcurrentSkipListMap;老代码兼容时退而求其次用 synchronizedSortedMap。永远先问"是否要并发"——这一个问题就把候选范围缩到 1~2 个(§2.3)。

排行榜延迟翻车的真相:流量翻倍后,某些热点 score 段(如 100 分附近)大量用户集中——TreeMap 的某个分支变得很深,加上 GC 不友好(红黑树的 parent 指针让 G1 写屏障频繁触发)。换成 ConcurrentSkipListMap 后并发读写不再阻塞、节点更小,P99 从 50ms 降到 0.8ms。

# 10.2 一次 put 的红黑路径

把 treeMap.put(15, "value") 这行的完整链路串起来——回扣本册前 20 篇:

T 0     put(15, "value") 调用
        [13篇] invokevirtual TreeMap.put
        
T+5ns   compare(key, root.key)
        [13篇] 接口分派到 Comparator 或自然顺序 compareTo
        
T+10ns  从根开始向下查找插入位置
        [01篇] 节点对象在堆中,遍历访问触发 cache miss
        红黑树指针密集,缓存友好性一般
        
T+50ns  找到插入位置(叶子)
        new Entry(15, "value", parent)  ← 染红
        [16篇] 新对象在 Eden 区分配
        
T+55ns  从插入点向上检查局面
        [§6.1] 5 种局面分类
        
T+60ns  假设触发局面 2(父红叔红)
        重染色:父黑、叔黑、爷红
        [§6.2] 上溯到爷爷继续检查
        
T+65ns  上溯到根之前,触发局面 4(父红叔黑、N 与父异侧)
        ① 先左旋 P  ← 转化为局面 3
        ② 再右旋 G  ← 完成调整
        [§5.1] rotateLeft 修改 6 条边
        
T+70ns  最终染色:根永远黑
        modCount++  size++
        [19篇] 与 ArrayList 的 modCount 同源 fail-fast
        
T+75ns  put 返回旧值(如果 key 已存在)
        
GC 视角:
        [16篇] Entry 对象在 Eden 区
        [17篇] G1 SATB 写屏障:因为 parent 指针修改,
              引发"老→新"跨代引用记录到 RememberedSet
              ★ 红黑树写屏障开销比 HashMap 高
        
JIT 视角:
        [14篇] put 是热点方法,C2 编译为机器码
        compareTo 内联,旋转操作也被内联
        但旋转分支多,难以被向量化优化
        
并发视角:
        [20篇] TreeMap 不是线程安全的
        多线程 put 会破坏 BST 性质(指针错乱)
        必须用 ConcurrentSkipListMap
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

这条时间线串起本篇的关键概念——一行普通的 put 背后,是 BST 查找、5 种局面分类、染色与旋转的精密配合。

# 10.3 设计哲学回扣

跳出技术细节,提炼三条贯穿红黑树设计的工程哲学:

  1. "近似平衡"优于"严格平衡":红黑树不是 AVL 那样的严格平衡——但常数级的旋转(≤3 次)让写入速度反超 AVL。这条原则在 GC(§17 G1 自适应步长)、并发计数(§9.1 LongAdder 弱一致 sum)、HashMap 树化阈值(§20 阈值 8 是概率门槛而非严格阈值)等多处都有体现——完美的敌人是足够好。

  2. 对称性是简化算法的关键:红黑树的所有调整规则都左右对偶——左旋有右旋、叔叔在左有叔叔在右、双黑兄在左有兄在右。对称性把代码量减半,把心智成本减半。这与 §5 篇 String 的不可变设计、§19 篇 Iterator 的统一契约一脉相承——好的抽象总是对称的。

  3. 数据结构与并发模型是耦合选择:红黑树的"动态形变"决定了它必然是单线程数据结构;跳表的"分层独立"决定了它天生适合 CAS。选数据结构就是选并发模型——这条原则解释了为什么 §20 篇的 ConcurrentHashMap 不基于 TreeMap、为什么本篇的 ConcurrentSkipListMap 不基于红黑树。先想清楚并发场景,再选数据结构,永远不要反过来。

# 10.4 有序容器速查表

最后一张表——任何有序容器选型 5 秒决策:

业务诉求 推荐 不推荐
单线程有序 Map(默认) TreeMap LinkedHashMap (按插入序非按 key 序)
高并发有序 Map ConcurrentSkipListMap synchronizedSortedMap
范围查询 (subMap) TreeMap / CSLM HashMap (无范围概念)
top-K 查询 TreeMap.firstKey/lastKey 全排序
优先队列(每次取最小) PriorityQueue (堆) TreeMap (有更好选择)
有序集合(去重) TreeSet List + sort
滑动窗口聚合 TreeMap.subMap List + filter
区间统计 (count of range) TreeMap (近似) 树状数组/线段树
并发优先队列 PriorityBlockingQueue CSLM (高级特性溢出)
老代码兼容 synchronizedSortedMap(TreeMap) 重写

有序容器心法三条:

1. 单线程有序,TreeMap 永远是默认答案
2. 并发有序,ConcurrentSkipListMap 是唯一选择
3. 用 PriorityQueue 而非 TreeMap 实现优先队列(堆比红黑树更适合)
1
2
3

至此第 21 篇完成——我们用 1 万字把红黑树五大性质、插入删除调整、跳表对比、ConcurrentSkipListMap 设计哲学讲透。但 Map 家族还有一员——LinkedHashMap 凭什么能既保插入序又支持 LRU?下一篇我们顺着"为什么 LinkedHashMap 既是 Map 又是双向链表"这条线,进入 第 22 篇:LinkedHashMap与LRU实现——把 access-order/insertion-order 双模式、removeEldestEntry 钩子、Caffeine 高级 LRU 一次讲透。

上次更新: 2026/06/10, 11:13:41
ConcurrentHashMap并发
LinkedHashMap与LRU实现

← ConcurrentHashMap并发 LinkedHashMap与LRU实现→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式