ConcurrentSkipListMap是天然线程安全且保持排序的Map,基于跳表实现,支持无锁读、细粒度锁写、高效范围查询;不支持null键,要求键可比较,Comparator需一致,排序策略构造后不可变。

ConcurrentSkipListMap 是 Java 并发包里少数几个「天然线程安全 + 保持自然排序」的 Map 实现,不用加锁、不用 Collections.synchronizedMap 包装,也不依赖外部排序逻辑 —— 它内部用跳表(skip list)结构直接支持并发插入/删除/遍历,同时键始终按 Comparable 或自定义 Comparator 排序。
为什么不用 TreeMap + synchronized?
TreeMap 虽然有序,但不支持并发:多线程下直接读写会抛 ConcurrentModificationException,而用 Collections.synchronizedSortedMap(new TreeMap()) 虽线程安全,却把所有操作串行化,性能差、易成瓶颈。相比之下,ConcurrentSkipListMap 的读操作完全无锁,写操作也只锁定局部层级,吞吐量高得多。
常见误用场景:
- 用
ConcurrentHashMap后再手动排序 —— 白费力气,且排序结果不保证实时一致 - 在循环中反复调用
keySet().toArray()再Arrays.sort()—— 破坏原子性,且无法响应新增键 - 误以为它支持
null键 —— 实际上会立即抛NullPointerException(连put(null, v)都不行)
ConcurrentSkipListMap 的构造与比较器选择
默认构造函数要求键实现 Comparable;若键类型无自然序或需自定义顺序,必须传入 Comparator。注意:这个 Comparator 必须满足「一致性」—— 即对同一对键,多次比较结果必须相同,否则跳表结构可能损坏。
立即学习“Java免费学习笔记(深入)”;
实操建议:
- 优先使用 lambda 表达式定义
Comparator,比如new ConcurrentSkipListMap<string integer>((a, b) -> b.compareTo(a))</string>(逆序) - 避免在
Comparator中调用可能阻塞或抛异常的方法(如远程调用、文件读取) - 若键是自定义类,确保
compareTo()与equals()逻辑协调 —— 虽然ConcurrentSkipListMap不依赖equals()判等(它用compareTo() == 0),但语义混乱容易引发调试困惑
哪些操作是真正「安全且有序」的?
它的「有序」体现在视图方法返回的集合(如 keySet()、entrySet()、values())和导航方法(如 ceilingKey()、headMap())上 —— 这些都反映当前状态下的实时排序,且本身线程安全。但要注意:
-
keySet().iterator()是弱一致性迭代器:不抛ConcurrentModificationException,但可能跳过或重复某些刚插入/删除的条目 -
subMap(from, to)返回的子映射仍是线程安全的,且保持排序,但其范围边界在创建时即固定,后续from/to对应的键被删掉不影响子映射有效性 -
putIfAbsent()、remove(key, value)等条件更新方法全部原子执行,无需额外同步
性能与内存开销的真实代价
跳表的平均时间复杂度是 O(log n),比 TreeMap 的 O(log n) 略高常数因子,但并发下实际表现更好。不过它比 ConcurrentHashMap 多约 2–3 倍内存占用(每个节点含多个指针),且初始化较慢(预分配层级结构)。所以:
- 键数量稳定在几千以内、读远多于写的场景,
ConcurrentSkipListMap是优选 - 若频繁做范围查询(如
tailMap(100))、或需要firstKey()/lastKey(),它比先用ConcurrentHashMap再维护一个ConcurrentSkipListSet键集更简洁可靠 - 不要为了「看起来有序」而滥用 —— 如果业务根本不需要导航或范围操作,
ConcurrentHashMap+ 外部排序更轻量
最易被忽略的一点:它的排序行为在运行时不可更改。一旦构造时用了某 Comparator,就不能中途切换;如果初始用默认构造(依赖 Comparable),后续往里放不实现该接口的键会直接失败。这种「静态有序性」既是保障,也是约束 —— 设计阶段就得想清楚键的可比性来源。


















