导读:本期聚焦于高建功创作的《Java里的ConcurrentSkipListMap是什么?基于跳表实现的可排序并发Map详解》,敬请观看详情。为什么ConcurrentHashMap无法保证键的顺序,而ConcurrentSkipListMap却可以做到有序且线程安全?答案藏在跳表这种数据结构里。本文将从跳表的基本原理讲起,分析多层索引如何让查询复杂度稳定在对数级别,再对比ConcurrentSkipListMap与ConcurrentHashMap在数据结构、性能表现、功能特性上的差异,最后通过示例代码演示遍历、范围查询、并发写入等典型用法,帮助你在需要有序并发场景下做出正确的容器选型。

在Java并发容器家族中,ConcurrentHashMap无疑是最常用的选择,但它有一个天然的局限:不保证键的遍历顺序。当你需要一个线程安全且按key排序的Map时,ConcurrentSkipListMap就是标准答案。它是JDK在1.6版本引入的并发容器,底层基于跳表(Skip List)实现,同时实现了NavigableMap接口,支持丰富的导航操作。本文将从原理、对比和实践三个层面,把这个容器彻底讲清楚。

Java里的ConcurrentSkipListMap是什么?基于跳表实现的可排序并发Map详解

跳表原理:为什么不用红黑树也能做到O(log n)

跳表是由William Pugh在1990年提出的一种概率性数据结构,核心思想是用空间换时间。单链表查找需要从头遍历,复杂度是O(n),而跳表在链表之上建立了多层索引:最底层是完整的有序链表,上层索引逐层抽取部分节点,每个节点以随机概率决定自己要出现在第几层。

查找时从最高层出发,如果当前节点的下一个节点比目标小,就继续向右走;否则下降一层继续找。这样每一层都能跳过大约一半的节点,整体查找次数期望值为O(log n),与平衡树的性能相当。下图是一个简化的跳表示意:假设要查找key为35的节点,从第3层开始,依次经过20(下降)、30(下降),最终在最底层定位到35,全程只比较了3次。

第3层:  head ------------------------------> 50
第2层:  head --------> 30 --------------------> 50
第1层:  head --> 20 --> 30 --> 40 --------> 50
第0层:  head -> 10 -> 20 -> 30 -> 35 -> 40 -> 50

相比红黑树,跳表最大的优势在于实现简单且天然适合并发。红黑树在插入删除时可能引发大范围的结构调整,需要全局加锁或复杂的锁方案;而跳表的插入和删除只影响相邻节点,可以通过CAS操作完成无锁化的层级连接。这正是Doug Lea选择跳表作为ConcurrentSkipListMap底层结构的原因。随机层数的生成也很简单,通常使用抛硬币的方式,代码大致如下:

// 判断节点应该插入到多少层,每层以1/4概率晋升
private int randomLevel() {
    int level = 0;
    while (rnd.nextInt() >= 0 && level < MAX_LEVEL) {
        level++;  // 概率性增加层数
    }
    return level;
}

与ConcurrentHashMap的对比:选型前必须搞清楚

两者的定位完全不同。ConcurrentHashMap基于哈希表实现,通过CAS加synchronized锁住桶头节点,写入吞吐量极高,查找是O(1)级别;ConcurrentSkipListMap基于跳表,写入和查找都是O(log n),还包含维护多层索引的额外开销。在纯粹的无序读写场景下,ConcurrentSkipListMap的性能通常只有ConcurrentHashMap的三分之一到一半左右。

但顺序性是它不可替代的价值。跳表本身有序,遍历时按键的自然顺序(或构造时传入的Comparator顺序)输出,支持firstKey、lastKey、floorKey、ceilingKey、headMap、tailMap、subMap等导航方法,这些在哈希结构上无从谈起。此外,ConcurrentSkipListMap的size方法需要遍历统计,复杂度是O(n)且不是常量时间,迭代器是弱一致性的,反映创建时刻之后的写入,但不会抛出ConcurrentModificationException。

选型可以简单总结为一句话:不需要顺序、追求极致吞吐,用ConcurrentHashMap;需要有序遍历、范围查询、排名类操作,用ConcurrentSkipListMap。与之配套的还有ConcurrentSkipListSet,本质上是对ConcurrentSkipListMap的包装,行为等价于线程安全的TreeSet。

实战用法:导航操作与并发遍历示例

先看基础用法。下面的代码演示了构造、排序遍历以及几个典型的导航方法。注意当key是自定义类型时,必须保证其compareTo方法与equals语义一致,否则容器内部状态可能出现混乱。

ConcurrentSkipListMap<Integer, String> map = new ConcurrentSkipListMap<>();
map.put(30, "三十");
map.put(10, "十");
map.put(20, "二十");

// 遍历结果按键升序输出:10、20、30
for (Map.Entry<Integer, String> entry : map.entrySet()) {
    System.out.println(entry.getKey() + " = " + entry.getValue());
}

System.out.println(map.firstKey());    // 10,最小的键
System.out.println(map.lastKey());     // 30,最大的键
System.out.println(map.floorKey(25));  // 20,小于等于25的最大键
System.out.println(map.ceilingKey(25)); // 30,大于等于25的最小键

范围查询是跳表结构的强项。headMap(K toKey)返回小于toKey的视图,tailMap(K fromKey)返回大于等于fromKey的视图,subMap则取闭开区间。这些方法返回的是原map的视图而非拷贝,视图上的修改会直接影响原容器,开销极小。

ConcurrentSkipListMap<Integer, String> map = new ConcurrentSkipListMap<>();
for (int i = 1; i <= 100; i++) {
    map.put(i, "value-" + i);
}

// 范围查询:取[20, 50)区间的数据,视图操作无需拷贝
NavigableMap<Integer, String> sub = map.subMap(20, true, 50, false);
sub.forEach((k, v) -> System.out.println(k + " -> " + v));

// 逆序视图,倒序遍历整个map
for (Integer k : map.descendingKeySet()) {
    // 从100到1依次输出
}

在多线程环境下使用时,可以直接放心地并发读写,无需任何外部同步。一个常见的应用场景是实时排行榜:多个线程并发更新分数,展示线程按分数倒序取出前N名。通过构造时传入倒序Comparator,配合headMap就能高效实现。

// 用跳表实现简单的并发排行榜,按分数从高到低排列
ConcurrentSkipListMap<Long, String> rank = new ConcurrentSkipListMap<>(Comparator.reverseOrder());

// 并发写入分数(key为分数,value为用户名)
new Thread(() -> rank.put(95L, "alice")).start();
new Thread(() -> rank.put(88L, "bob")).start();
new Thread(() -> rank.put(92L, "carol")).start();

// 取前两名:由于是倒序,最小的key就是榜首之后的边界
NavigableMap<Long, String> top = rank.headMap(90L, true);
top.forEach((score, user) -> System.out.println(user + ": " + score));

使用时的注意事项

第一,性能敏感场景要掂量索引开销。每次插入都要生成随机层数、维护索引链,高并发写入下CAS重试也可能带来竞争,官方文档也明确指出它是排序场景下的选择而非通用首选。第二,size和isEmpty不是O(1),跨方法组合调用(比如先判断size再get)在并发下没有原子性保证,需要原子语义时应使用putIfAbsent、compute、merge这类复合方法。

第三,key必须可比较且比较逻辑稳定。要么实现Comparable接口,要么构造时提供Comparator,比较过程中如果依赖可变字段,会导致节点定位错误甚至数据丢失。第四,null约束很严格:key和value都不允许为null,传入null会直接抛出NullPointerException,这一点比HashMap严格得多,迁移旧代码时要特别留意。

总结一下,ConcurrentSkipListMap用概率性的跳表结构换来了与TreeMap等价的有序语义,同时通过CAS实现了无锁并发。理解它,不仅能解决有序并发这一类具体问题,更能体会到数据结构设计在并发编程中的权衡艺术。

ConcurrentSkipListMap跳表并发Map修改时间:2026-08-31 18:00:40

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。