ConcurrentSkipListMap 并发安全实现
想了解
ConcurrentSkipListMap 实现并发安全的底层原理,我会从它的核心数据结构、并发控制机制、核心操作逻辑三个维度,用通俗易懂的方式讲解,同时对比它和 ConcurrentHashMap 的差异,帮你理解它的设计思路和适用场景。
一、先明确核心定位
ConcurrentSkipListMap 是 Java 中有序且高并发的 Map 实现类,基于跳表(SkipList) 实现,核心特点是:- 有序性:和
TreeMap一样,按键自然排序或自定义排序; - 并发安全:无锁化的并发控制(CAS + 自旋),支持高并发的读写操作;
- 性能:高并发下比
Collections.synchronizedSortedMap(new TreeMap<>())高效得多。
二、核心数据结构:跳表(SkipList)
要理解它的并发实现,首先要懂跳表的基础结构 —— 跳表是 “分层的链表”,可以看作 “有序链表的优化版”,能把查找、插入、删除的时间复杂度降到
O(log n)。跳表的基本结构(简化版)
plaintext
层级4:head → ----------------------------> 60 → null
层级3:head → --------> 30 → ------------> 60 → null
层级2:head → --------> 30 → 40 → 50 → 60 → null
层级1:head → 10 → 20 → 30 → 40 → 50 → 60 → null (原始有序链表)
- 底层:是一个有序的单向链表(层级 1),保存所有数据;
- 上层:是 “索引层”,层级越高,节点越少,用于快速定位底层节点(类似数据库的索引);
- 头节点(head):每个层级都有头节点,统一管理各层链表。
跳表的优势
相比红黑树(
TreeMap/ConcurrentHashMap 桶内结构),跳表的节点插入 / 删除逻辑更简单,更容易实现无锁的并发控制 —— 红黑树的旋转操作在并发下很难保证原子性,而跳表只需修改节点的指针,配合 CAS 就能实现。三、ConcurrentSkipListMap 的并发实现核心机制
ConcurrentSkipListMap 完全基于无锁化设计(CAS + 自旋) 实现并发安全,没有使用 synchronized 或 Lock 锁,核心思路是:对单个节点的指针操作通过 CAS 保证原子性,通过自旋处理竞争,通过 “标记删除” 解决并发删除的冲突。1. 核心设计原则
- 无锁化:所有核心操作(插入、删除、查找)都通过 CAS 原子操作完成,避免锁竞争带来的性能开销;
- 弱一致性:迭代器、查找操作不会锁定整个结构,读取到的可能是 “稍旧” 的数据,但保证不抛出异常;
- 分层并发:跳表的层级结构让不同层级的操作可以部分并行,进一步降低冲突。
2. 核心操作的并发实现(简化解析)
下面以插入操作为例,拆解并发安全的实现逻辑(删除 / 查找逻辑类似):
步骤 1:查找插入位置(无锁遍历)
- 从跳表的最高层头节点开始,逐层向下遍历,找到底层链表中 “小于目标键的最后一个节点”(前驱节点);
- 遍历过程中不锁定任何节点,若遇到节点被修改(如指针变化),则重新遍历(自旋),保证最终找到正确的位置。
步骤 2:创建新节点,CAS 插入(核心并发保障)
java
// 简化版插入逻辑(核心思想)
private V doPut(K key, V value, boolean onlyIfAbsent) {
// 1. 查找插入位置,获取前驱节点(prev)和后继节点(next)
Node<K,V> prev, next;
int hash = key.hashCode();
for (;;) { // 自旋,直到插入成功
// 查找前驱节点和后继节点
if (findPredecessor(key, hash, out: prev, next)) {
// 键已存在:CAS 更新值
if (next.casValue(next.value, value)) {
return next.value;
}
} else {
// 键不存在:创建新节点
Node<K,V> newNode = new Node<>(hash, key, value);
// CAS 替换前驱节点的 next 指针,插入新节点
if (prev.casNext(next, newNode)) {
// 3. 随机提升新节点到上层(跳表的层级随机化)
promote(newNode);
return null;
}
}
// CAS 失败(其他线程修改了 prev 的 next 指针),自旋重试
}
}
步骤 3:关键并发保障点解析
-
CAS 原子操作:
- 核心方法:
casNext()(原子更新节点的 next 指针)、casValue()(原子更新节点的值); - 逻辑:比较当前节点的指针 / 值是否等于预期值,若是则更新为新值,整个过程由 CPU 保证原子性,避免多线程竞争导致的指针错乱。
- 核心方法:
-
自旋重试:
- 当 CAS 操作失败(说明其他线程正在修改同一个节点),不会阻塞,而是进入下一轮循环(自旋),重新查找位置并尝试插入;
- 自旋是 “无锁并发” 的典型特征,相比加锁,减少了线程切换的开销。
-
标记删除(解决并发删除冲突):
- 删除节点时,不会直接移除,而是先通过 CAS 给节点打上 “删除标记”(标记节点的
value为null或deleted); - 遍历过程中会跳过带删除标记的节点,后台线程(或后续操作)再清理这些标记节点,避免并发删除导致的链表断裂。
- 删除节点时,不会直接移除,而是先通过 CAS 给节点打上 “删除标记”(标记节点的
-
层级提升的并发安全:
- 新节点插入底层后,会随机决定是否提升到上层(跳表的层级随机化规则);
- 提升过程也是通过 CAS 逐层更新上层节点的指针,若某一层 CAS 失败,停止提升(不影响底层数据),保证整体结构稳定。
3. 读操作的并发保障
- 读操作(
get()、迭代)完全无锁,直接遍历跳表; - 若遍历过程中遇到节点被修改(如指针变化、节点被标记删除),则重新遍历,最终读取到 “一致” 的数据(可能是修改前的旧值,弱一致性);
- 所有节点的指针和值都用
volatile修饰,保证多线程间的可见性(一个线程修改后,其他线程能立即看到)。
四、ConcurrentSkipListMap vs ConcurrentHashMap(并发特性对比)
| 维度 | ConcurrentSkipListMap | ConcurrentHashMap(JDK8) |
|---|---|---|
| 数据结构 | 跳表(SkipList) | 数组 + 链表 / 红黑树 |
| 有序性 | 天然有序(按键排序) | 无序(桶内有序,整体无序) |
| 并发实现 | 无锁(CAS + 自旋) | 轻量级锁(CAS + synchronized 锁桶节点) |
| 时间复杂度 | 所有操作 O (log n) | 平均 O (1),最坏 O (log n)(桶内红黑树) |
| 适用场景 | 有序并发场景(如排行榜、区间查询) | 无序高并发读写场景(如缓存、通用存储) |
| null 支持 | 键 / 值都不允许 null | 键 / 值都不允许 null |
五、实战示例:ConcurrentSkipListMap 的并发使用
java
import java.util.Map;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.CountDownLatch;
public class ConcurrentSkipListMapTest {
private static final int THREAD_COUNT = 10;
private static final int OP_COUNT = 1000;
public static void main(String[] args) throws InterruptedException {
// 有序且并发安全的 Map
Map<Integer, String> skipListMap = new ConcurrentSkipListMap<>();
CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
long start = System.currentTimeMillis();
// 多线程写入
for (int i = 0; i < THREAD_COUNT; i++) {
int threadId = i;
new Thread(() -> {
for (int j = threadId * OP_COUNT; j < (threadId + 1) * OP_COUNT; j++) {
skipListMap.put(j, "value-" + j);
}
latch.countDown();
}).start();
}
latch.await();
long end = System.currentTimeMillis();
// 验证有序性和数据完整性
System.out.println("最终大小:" + skipListMap.size()); // 输出 10000
System.out.println("第一个键:" + skipListMap.firstKey()); // 输出 0
System.out.println("最后一个键:" + skipListMap.lastKey()); // 输出 9999
System.out.println("耗时:" + (end - start) + "ms");
}
}
输出结果(示例):
plaintext
最终大小:10000
第一个键:0
最后一个键:9999
耗时:85ms
- 数据完整(无丢失),且按键有序排列,体现了
ConcurrentSkipListMap的核心优势。
总结
- 核心并发机制:
ConcurrentSkipListMap基于跳表和无锁化设计(CAS + 自旋) 实现并发安全,通过 CAS 保证节点指针 / 值的原子更新,自旋处理竞争,标记删除解决并发删除冲突; - 核心优势:天然有序 + 无锁并发,适合需要 “有序 + 高并发” 的场景(如排行榜、区间查询);
- 选型对比:有序并发选
ConcurrentSkipListMap,无序高并发选ConcurrentHashMap,单线程有序选TreeMap。
*
备注:公众号清汤袭人能找到我,那是随笔的地方
备注:公众号清汤袭人能找到我,那是随笔的地方

浙公网安备 33010602011771号