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 + 自旋) 实现并发安全,没有使用 synchronizedLock 锁,核心思路是:对单个节点的指针操作通过 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:关键并发保障点解析
  1. CAS 原子操作:
     
    • 核心方法:casNext()(原子更新节点的 next 指针)、casValue()(原子更新节点的值);
    • 逻辑:比较当前节点的指针 / 值是否等于预期值,若是则更新为新值,整个过程由 CPU 保证原子性,避免多线程竞争导致的指针错乱。
     
  2. 自旋重试:
     
    • 当 CAS 操作失败(说明其他线程正在修改同一个节点),不会阻塞,而是进入下一轮循环(自旋),重新查找位置并尝试插入;
    • 自旋是 “无锁并发” 的典型特征,相比加锁,减少了线程切换的开销。
     
  3. 标记删除(解决并发删除冲突):
     
    • 删除节点时,不会直接移除,而是先通过 CAS 给节点打上 “删除标记”(标记节点的 valuenulldeleted);
    • 遍历过程中会跳过带删除标记的节点,后台线程(或后续操作)再清理这些标记节点,避免并发删除导致的链表断裂。
     
  4. 层级提升的并发安全:
     
    • 新节点插入底层后,会随机决定是否提升到上层(跳表的层级随机化规则);
    • 提升过程也是通过 CAS 逐层更新上层节点的指针,若某一层 CAS 失败,停止提升(不影响底层数据),保证整体结构稳定。

3. 读操作的并发保障

  • 读操作(get()、迭代)完全无锁,直接遍历跳表;
  • 若遍历过程中遇到节点被修改(如指针变化、节点被标记删除),则重新遍历,最终读取到 “一致” 的数据(可能是修改前的旧值,弱一致性);
  • 所有节点的指针和值都用 volatile 修饰,保证多线程间的可见性(一个线程修改后,其他线程能立即看到)。

四、ConcurrentSkipListMap vs ConcurrentHashMap(并发特性对比)

 
维度ConcurrentSkipListMapConcurrentHashMap(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 的核心优势。

总结

  1. 核心并发机制:ConcurrentSkipListMap 基于跳表和无锁化设计(CAS + 自旋) 实现并发安全,通过 CAS 保证节点指针 / 值的原子更新,自旋处理竞争,标记删除解决并发删除冲突;
  2. 核心优势:天然有序 + 无锁并发,适合需要 “有序 + 高并发” 的场景(如排行榜、区间查询);
  3. 选型对比:有序并发选 ConcurrentSkipListMap,无序高并发选 ConcurrentHashMap,单线程有序选 TreeMap
posted @ 2026-01-23 10:04  野鹤闲人  阅读(0)  评论(0)    收藏  举报