一、整体定位:它是什么?
ConcurrentSkipListMap是一个可扩展、线程安全、按键排序的并发 Map,实现了ConcurrentNavigableMap接口。
- 底层数据结构:跳表(Skip List),而非红黑树(如
TreeMap)。 - 排序方式:支持自然排序(
Comparable)或自定义Comparator。 - 并发模型:无锁(Lock-Free),通过CAS(Compare-And-Swap)实现线程安全。
- 作者:Doug Lea(Java 并发包之父)。
- 引入版本:JDK 1.6。
✅适用场景:
需要高并发读写 + 键有序 + 范围查询(如 subMap, headMap)的场景,例如:
- 高频交易系统中的订单簿(按价格排序)
- 缓存淘汰策略(如 LRU + 有序)
- 分布式系统中的有序任务队列
二、为什么用“跳表”而不是“红黑树”?
这是 Doug Lea 在注释中明确解释的关键设计选择:
❝There are no known efficient lock-free insertion and deletion algorithms for search trees.❞
对比分析:
| 特性 | 红黑树(TreeMap) | 跳表(ConcurrentSkipListMap) |
|---|---|---|
| 单线程性能 | O(log n),常数因子小 | O(log n),常数因子略大 |
| 并发实现难度 | 极难实现无锁(需复杂指针操作) | 天然适合无锁(链表 + 指针 CAS) |
| 实现复杂度 | 高(旋转、平衡) | 中(概率性结构,逻辑清晰) |
| 内存局部性 | 差(树节点分散) | 较好(链表连续,跳表索引缓存友好) |
| 范围遍历 | 支持 | 支持,且正向更快 |
✅结论:
在高并发无锁场景下,跳表比平衡树更容易实现、更稳定、更可预测。
三、核心并发机制:如何做到线程安全?
1.双层删除标记(Two-Phase Deletion)
这是最关键的无锁删除算法,避免 ABA 问题和中间状态不一致。
删除一个节点n(前驱b,后继f)分三步:
初始: ... → [b] → [n] → [f] → ... Step 1: 将 n.value 设为 null(逻辑删除) → 所有 get/containsKey 都认为 n 不存在 Step 2: 在 n 后插入一个 marker 节点 ... → [b] → [n] → [marker] → [f] → ... → 阻止其他线程在 n 后插入新节点 Step 3: CAS b.next 跳过 [n] 和 [marker] ... → [b] → [f] → ... → 物理删除完成,n 可被 GC✅优势:
- 其他线程在遍历时若发现
value == null,会主动“帮助”完成删除(helpDelete),避免阻塞。- 使用普通
Node作为 marker(value == this),节省空间,避免AtomicMarkedReference的开销。
2.值为 null 表示逻辑删除
- 正常节点:
value是实际值(非 null) - 已删除节点:
value = null - Marker 节点:
value = this(自引用)
💡 这与
Map.get(key)返回null表示“不存在”完美契合,无需额外标记。
3.索引层(Index Levels)独立维护
- 基础层(base level):存储所有键值对,用上述无锁链表。
- 索引层(index levels):多层“快速通道”,每层是基础层的子集。
- 索引插入/删除是惰性的:先完成基础层操作,再异步更新索引。
- 减少 CAS 冲突窗口
- 避免索引节点内存泄漏(确保删除后索引不可达)
📌 跳表高度动态调整:插入时可能提升层级,删除时可能降低(启发式)。
4.弱一致性迭代器(Weakly Consistent Iterators)
- 迭代器不会抛出
ConcurrentModificationException - 能看到调用 iterator() 之前已存在的元素
- 可能看到、也可能看不到并发修改的元素
- 不会重复遍历同一元素
✅ 适合高并发读多写少场景,牺牲强一致性换取高性能。
四、重要注意事项(来自 Javadoc)
| 特性 | 说明 |
|---|---|
size()不是 O(1) | 需遍历所有元素,结果可能是近似值(遍历时被修改) |
不支持null键/值 | 因get(key)返回null表示“不存在”,无法区分 |
Entry.setValue()不支持 | 返回的Map.Entry是快照,需用replace()修改 |
| 批量操作非原子 | putAll,clear,equals等不是原子操作 |
| 正向遍历更快 | 升序(ascending)比降序(descending)性能更好 |
五、性能与空间
- 时间复杂度:
get,put,remove,containsKey平均O(log n) - 空间开销:约 25% 节点有索引(p=0.5),总空间略小于
TreeMap - 并发性能:远优于
Collections.synchronizedMap(new TreeMap<>())
六、总结:如何理解这段代码?
ConcurrentSkipListMap是 Doug Lea 利用“跳表 + 无锁链表 + 双阶段删除 + 弱一致性”设计出的一个高性能并发有序 Map。它牺牲了部分强一致性(如 size() 不精确、迭代器弱一致),换来了极高的并发吞吐量和可扩展性,是高并发场景下替代TreeMap的首选。
关键创新点:
- 用普通 Node 实现删除标记,避免 AtomicMarkedReference 开销;
- 逻辑删除(value=null)与物理删除分离,支持高效并发;
- 索引层惰性更新,减少竞争,防止内存泄漏;
- 帮助机制(helpDelete),让其他线程协助完成删除,避免阻塞。
类比记忆:
想象一条高速公路(基础链表),上面每隔一段就有立交桥(索引层)。
当你要拆除一个出口(删除节点),先挂“施工中”牌子(value=null),
再建个临时隔离带(marker),最后把路直接接过去(CAS 跳过)。
其他司机(线程)看到施工牌,会主动帮你清理现场(helpDelete)!
这就是ConcurrentSkipListMap的智慧。