← 返回题目列表

ConcurrentSkipListMap 是什么?跳表是怎么工作的?为什么不用红黑树?

困难 第 29 / 30 题 更新于 2026/07/27
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 无锁、红黑树旋转是全局难无锁」。