ConcurrentSkipListMap 是什么?跳表是怎么工作的?为什么不用红黑树?
简化版
ConcurrentSkipListMap 是一个「并发安全 + 有序」的 Map——相当于「线程安全版的 TreeMap」(TreeMap 有序但非线程安全,ConcurrentHashMap 线程安全但无序,它两者兼得)。它底层不是红黑树,而是跳表(Skip List)——一种「多层链表」结构:最底层是一个有序链表(含所有元素),上面几层是「跳跃索引」(越高层越稀疏),查找时从最高层开始「能跳就跳、跳过头就下一层」,把链表的 O(n) 查找降到 O(log n)。为什么并发场景用跳表不用红黑树?因为跳表更容易实现无锁并发——它的插入/删除只需改几个指针(局部操作),能用 CAS 实现无锁;而红黑树的旋转/变色是「全局性调整」(改动范围大),并发下加锁复杂、难以无锁化。所以「有序 + 高并发」选跳表。
详细版
三种 Map 的定位:
| Map | 有序 | 线程安全 | 底层 |
|---|---|---|---|
| HashMap | 无序 | 否 | 哈希表 |
| TreeMap | 有序 | 否 | 红黑树 |
| ConcurrentHashMap | 无序 | 是 | 哈希表 + CAS/synchronized |
| ConcurrentSkipListMap | 有序 | 是 | 跳表 |
// ConcurrentSkipListMap:并发安全 + 有序(按 key 排序)
ConcurrentNavigableMap<Integer, String> map = new ConcurrentSkipListMap<>();
map.put(3, "c"); map.put(1, "a"); map.put(2, "b");
// 遍历有序:1=a, 2=b, 3=c(像 TreeMap)
// 且线程安全(多线程并发读写无需外部加锁)
// 还有 TreeMap 的导航方法:firstKey/lastKey/floorKey/ceilingKey/subMap
跳表结构:
多层链表,越往上越稀疏(索引层),最底层含所有元素:
L3: 1 ──────────────────→ 9
L2: 1 ────→ 5 ──────────→ 9
L1: 1 → 3 → 5 → 7 ──────→ 9
L0: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 9 (底层:完整有序链表)
查找 7:从 L3 开始
L3: 1→9,9>7 跳过头 → 下降到 L2 的 1
L2: 1→5,5<7 前进到 5;5→9,9>7 → 下降到 L1 的 5
L1: 5→7,找到!
→ 像"跳着找",O(log n)
⚠️ 跳表的层数是随机决定的——插入一个新元素时,用「抛硬币」的方式随机决定它「向上建几层索引」(每层有约 50% 概率继续向上)。这个随机性让跳表平均保持 O(log n)(各层元素数大致按 1/2 递减,像平衡树),且不需要像红黑树那样做复杂的旋转来维持平衡——随机化天然让结构大致平衡。这是跳表「用随机化替代显式平衡操作」的精妙之处。
完整版教学
一、ConcurrentSkipListMap 的定位:有序 + 并发
要理解 ConcurrentSkipListMap,先看它填补的空白——「既要有序、又要线程安全」的 Map:
已有的 Map 各有短板:
HashMap:快,但无序、非线程安全
TreeMap:有序(红黑树),但非线程安全
ConcurrentHashMap:线程安全,但无序(哈希表)
→ 缺一个"有序 + 线程安全"的!
ConcurrentSkipListMap 补上:
有序(按 key 排序,像 TreeMap)+ 线程安全(并发读写无需外部锁)
→ "线程安全版的 TreeMap"
所以 ConcurrentSkipListMap 的定位是「并发环境下需要有序 Map」的场景——比如并发的排行榜、按时间排序的并发事件表、需要范围查询(subMap)的并发场景。它实现了 ConcurrentNavigableMap(并发 + 导航),既有 ConcurrentHashMap 的线程安全,又有 TreeMap 的有序和导航方法(floor/ceiling/subMap 等)。理解「ConcurrentSkipListMap = 有序 + 线程安全 = 线程安全版 TreeMap」,就理解了它填补的空白——它是「有序并发 Map」的答案。而它用跳表实现,是这道题的核心。
二、跳表是什么:多层链表加速查找
跳表(Skip List) 是一种巧妙的数据结构——用「多层链表 + 跳跃索引」把有序链表的查找从 O(n) 降到 O(log n):
问题:一个有序链表,查找元素要 O(n)(从头一个个找),太慢
(虽然有序,但链表不能像数组那样二分查找)
跳表的解法:在有序链表之上,建"多层索引"
最底层 L0:完整的有序链表(所有元素)
L1:从 L0 里"隔一个取一个"建索引(稀疏一半)
L2:从 L1 里再"隔一个取一个"(更稀疏)
...越往上越稀疏
查找时:从最高层开始
在当前层往前走,走到"下一个就超过目标"时,下降一层继续
→ 高层"大步跳过"很多元素,低层"精细定位"
→ 每层大约排除一半元素,总共 O(log n) 步
跳表的核心思想是「用空间换时间——建多层索引,让查找能『跳着走』」。就像看书时用「章 → 节 → 段」的层级目录快速定位,而不是一页页翻。最高层稀疏(大步跳),越往下越密(精细找),每下降一层排除约一半,所以 O(log n)。理解「跳表用多层链表索引让有序链表查找变 O(log n)、越高层越稀疏」,就理解了跳表的本质——它是「链表的二分查找版」。
三、跳表的插入与随机层数
跳表插入元素时,一个关键设计是「用随机决定新元素建几层索引」:
插入元素 x:
1. 先在底层 L0 找到 x 应该插入的位置,插入
2. 然后"抛硬币"决定要不要给 x 建上层索引:
- 抛到正面(50%)→ 在 L1 也建一个索引,继续抛
- 又正面(50%)→ 在 L2 也建,继续抛...
- 抛到反面 → 停止(x 就到这一层)
→ 结果:约 1/2 的元素有 L1 索引、1/4 有 L2、1/8 有 L3...
天然形成"越高层越稀疏"的结构(各层约按 1/2 递减)
为什么用随机?因为随机化能让跳表『平均』保持平衡(各层元素数大致按几何级数递减),而不需要像红黑树那样做复杂的旋转/变色来显式维持平衡。这是跳表最精妙的地方——用「随机层数」这个简单机制,替代了平衡树复杂的平衡操作。插入只需:底层插入 + 按随机层数往上建索引(都是局部的指针操作),没有旋转、没有全局重排。所以跳表实现比红黑树简单得多,性能也是平均 O(log n)。理解「跳表用随机层数天然维持平衡、插入只是局部指针操作无需旋转」,就理解了它为什么简单又高效——随机化是它的灵魂。
四、为什么并发用跳表不用红黑树
这是本题的核心追问——为什么 ConcurrentSkipListMap 用跳表,而不是像 TreeMap 那样用红黑树?关键在「哪个更容易实现无锁并发」:
红黑树的并发难点:
红黑树插入/删除后要"旋转 + 变色"来维持平衡
→ 旋转是"全局性调整":一次旋转可能改动树的多个节点、影响范围大
→ 并发下:多个线程同时改树,旋转会互相冲突
→ 要么加大锁(性能差),要么无锁实现极其复杂(几乎不可行)
跳表的并发优势:
跳表插入/删除只需改"几个相邻节点的指针"(局部操作)
→ 每层是独立的链表,改动是"局部的、指针级的"
→ 能用 CAS 无锁地改指针(像无锁链表)
→ 并发下:不同位置的操作互不干扰,能高效无锁并发
核心原因:红黑树的平衡操作(旋转)是「全局性」的(改动范围大、难以无锁),跳表的操作是「局部性」的(只改几个指针、能用 CAS 无锁)。所以:单线程有序 Map 用红黑树(TreeMap,红黑树内存更省、无随机开销);并发有序 Map 用跳表(ConcurrentSkipListMap,跳表易无锁化)。ConcurrentSkipListMap 正是用「跳表的局部指针操作 + CAS」实现了高效的无锁(或低锁)并发。理解「红黑树旋转是全局操作难无锁、跳表是局部指针操作易无锁、所以并发有序 Map 用跳表」,就答出了这道题的精髓——这是「为什么并发选跳表」的根本。
五、性能与复杂度
跳表和红黑树的复杂度对比:
| 操作 | 红黑树(TreeMap) | 跳表(ConcurrentSkipListMap) |
|---|---|---|
| 查找 | O(log n) | O(log n)(平均) |
| 插入 | O(log n) | O(log n)(平均) |
| 删除 | O(log n) | O(log n)(平均) |
| 有序遍历 | O(n) | O(n) |
| 范围查询 | O(log n + k) | O(log n + k) |
| 并发 | 难无锁 | 易无锁 |
| 空间 | 较省 | 稍多(多层索引指针) |
| 平衡方式 | 显式旋转(确定 O(log n)) | 随机层数(平均 O(log n)) |
关键对比:复杂度上两者都是 O(log n)(跳表是「平均」、红黑树是「最坏」——跳表理论上有极小概率退化,但实际几乎不会);跳表的优势在「易无锁并发」,代价是「多层索引占稍多空间」和「随机化(非确定性)」。所以:单线程用 TreeMap(红黑树,空间省、确定性)、并发用 ConcurrentSkipListMap(跳表,易并发)。别在单线程场景用 ConcurrentSkipListMap(并发的开销和空间浪费不值得)。理解「跳表和红黑树复杂度相当、跳表易并发但占空间、单线程用红黑树并发用跳表」,就掌握了两者的选型。
六、应用场景与相关类
ConcurrentSkipListMap 的应用和相关类:
ConcurrentSkipListMap 的应用场景:
- 并发环境下需要"有序"的 Map(按 key 排序遍历、范围查询)
- 并发排行榜(按分数排序、并发更新)
- 按时间排序的并发事件/任务表
- 需要 floor/ceiling/subMap 等导航方法的并发场景
相关类:
ConcurrentSkipListSet:基于 ConcurrentSkipListMap 的 Set(有序 + 并发的 Set)
= 线程安全版的 TreeSet
(就像 TreeSet 基于 TreeMap、HashSet 基于 HashMap)
选型总结(有序 Map/Set):
单线程有序 Map → TreeMap;并发有序 Map → ConcurrentSkipListMap
单线程有序 Set → TreeSet;并发有序 Set → ConcurrentSkipListSet
实践中 ConcurrentSkipListMap 用得不算多(「并发 + 有序」的需求本身不常见),但它是「有序并发 Map」的标准答案。它的存在也补全了 JUC 并发容器体系——ConcurrentHashMap(并发无序)+ ConcurrentSkipListMap(并发有序)覆盖了并发 Map 的两种需求。ConcurrentSkipListSet 则是它的 Set 版本(有序并发 Set)。理解「ConcurrentSkipListMap 用于并发有序场景、ConcurrentSkipListSet 是它的 Set 版、和 TreeMap/TreeSet 对应」,就掌握了它在并发容器体系里的位置。
记忆钩子:「ConcurrentSkipListMap = 有序 + 线程安全 = 线程安全版 TreeMap(补了『有序并发 Map』的空白);底层是跳表(多层链表,越高层越稀疏,最底层完整有序链表,查找从高层跳着找 O(log n),插入用随机层数天然维持平衡、无需旋转);并发用跳表不用红黑树,因为跳表插入删除是局部指针操作(能 CAS 无锁),红黑树旋转是全局操作(难无锁);单线程有序用 TreeMap、并发有序用它;ConcurrentSkipListSet 是它的 Set 版」。
七、常见误区与追问
- 误区:ConcurrentSkipListMap 底层是红黑树。 是跳表(多层链表)——不用红黑树是因为跳表的局部指针操作更容易实现无锁并发,而红黑树的旋转是全局操作难无锁。
- 误区:跳表和红黑树的查找复杂度不同。 都是 O(log n)——跳表是「平均」O(log n)(随机层数保证)、红黑树是「最坏」O(log n)(显式平衡保证);实际性能相当。
- 误区:跳表需要复杂的平衡操作。 不需要——跳表用「随机层数」(插入时抛硬币决定建几层索引)天然维持大致平衡,不像红黑树要旋转/变色,实现简单得多。
- 误区:单线程也该用 ConcurrentSkipListMap 图有序。 单线程有序用 TreeMap(红黑树,空间更省、确定性);ConcurrentSkipListMap 有并发开销和多层索引的空间浪费,只在「并发 + 有序」时才用。
- 追问:为什么 ConcurrentSkipListMap 用跳表而不用红黑树? 跳表插入/删除只需改几个相邻节点的指针(局部操作,能用 CAS 无锁实现),而红黑树的旋转/变色是全局性调整(改动范围大、并发下难以无锁),所以有序并发容器用更易无锁的跳表。
- 追问:跳表是怎么保证 O(log n) 的? 多层链表,越高层越稀疏(各层元素约按 1/2 递减,靠插入时随机层数实现);查找从最高层开始,每层排除约一半元素、跳过头就下降一层,总共约 O(log n) 步。
- 追问:跳表的层数是怎么决定的? 随机——插入元素时用「抛硬币」方式,每次约 50% 概率继续向上建一层索引,直到反面停止;这个随机化让各层元素数大致按几何级数递减,天然维持平衡、无需显式旋转。
八、加强记忆
ConcurrentSkipListMap 是「有序 + 线程安全」的 Map(= 线程安全版的 TreeMap),填补了「HashMap 无序非安全、TreeMap 有序非安全、ConcurrentHashMap 安全无序」都覆盖不到的「有序并发 Map」空白,实现 ConcurrentNavigableMap(有 floor/ceiling/subMap 导航方法)。它底层是跳表(Skip List)——「多层链表」结构:最底层是完整有序链表,上面是越来越稀疏的跳跃索引,查找从最高层开始「能跳就跳、跳过头就下降一层」,把有序链表的 O(n) 降到 O(log n);插入时用随机层数(抛硬币决定建几层索引)天然维持大致平衡,无需红黑树那样的旋转。为什么并发用跳表不用红黑树(核心):跳表的插入/删除只需改几个相邻节点的指针(局部操作,能用 CAS 无锁实现),而红黑树的旋转/变色是全局性调整(改动范围大、并发下难以无锁)——所以有序并发容器选更易无锁的跳表。复杂度上两者都 O(log n)(跳表平均、红黑树最坏),跳表易并发但占稍多空间。选型:单线程有序用 TreeMap(红黑树)、并发有序用 ConcurrentSkipListMap(跳表);ConcurrentSkipListSet 是它的 Set 版。一句话「ConcurrentSkipListMap 是有序并发 Map=线程安全 TreeMap、底层跳表(多层链表 O(log n)、随机层数免旋转),并发用跳表因局部指针操作易 CAS 无锁、红黑树旋转是全局难无锁」。