ConcurrentMap接口及其实现类深度解析
ConcurrentMap 接口是 Java 并发包(JUC)中定义的线程安全 Map 接口,继承自 Map 接口,提供了原子性操作方法(如 putIfAbsent
、remove
、replace
等)。本文将深入分析其两大核心实现类:ConcurrentHashMap 和 ConcurrentSkipListMap,并探讨它们的设计原理、适用场景及性能差异。
ConcurrentHashMap:高并发哈希表的进化之路
JDK7 与 JDK8 的架构差异
JDK7 分段锁架构
- 分段锁(Segment):将整个 Map 分为 16 个 Segment(类似小型 HashMap),每个 Segment 独立加锁,不同 Segment 可并发读写,提高并发度;
- 两次哈希:第一次定位 Segment,第二次定位元素所在链表;
- 缺点:锁粒度较大,并发度受限于 Segment 数量,且结构复杂。
JDK8 数组 + 链表 + 红黑树
- 取消分段锁:采用 Node 数组 + 链表 / 红黑树结构,直接对数组节点加锁(锁粒度更小);
- CAS + synchronized:使用 CAS 处理无锁场景(如初始化、空槽插入),synchronized 处理竞争场景(如链表插入、红黑树操作);
- 优化点:
- 链表长度超过 8 时自动转换为红黑树(O (n) → O (logn));
- 扩容时多线程协助迁移数据,提升效率。
JDK8 核心源码解析
重要变量与机制
1 | // 哈希表主体,volatile 保证可见性 ,初始化阶段是在第一次插入的时候,容量总是2的次幂 |
Node 节点结构:
1 | static class Node<K,V> implements Map.Entry<K,V> { |
put 方法核心流程
1 | final V putVal(K key, V value, boolean onlyIfAbsent) { |
关键点:
- CAS 无锁插入:槽为空时直接 CAS 插入,避免加锁;
- synchronized 细粒度锁:仅对槽头节点加锁,不影响其他槽;
- 链表转红黑树:长度超过阈值时自动转换,提升查询效率;
- 多线程扩容:通过
helpTransfer
方法让多个线程协助迁移数据。
扩容机制
当元素数量超过阈值(sizeCtl)时触发扩容,核心流程:
- 创建两倍容量的新表
nextTable
; - 将原表数据迁移至新表,迁移期间允许并发读写;
- 多线程协作迁移:通过
transferIndex
控制各线程负责的槽范围。
computeIfAbsent 与 putIfAbsent 的差异
场景对比
方法 | Key 存在时 | Key 不存在时 | 处理 null 值 |
---|---|---|---|
putIfAbsent |
返回旧值,不执行计算 | 计算并插入新值 | 允许插入 null |
computeIfAbsent |
返回旧值,不执行计算 | 计算并插入新值 | 不允许计算结果为 null |
性能差异
当 Value 计算代价高昂时:
1 | // 可能浪费计算资源(无论 Key 是否存在都执行 valueSupplier) |
ConcurrentSkipListMap:支持并发排序的跳表实现
跳表(Skip List)原理
跳表是一种随机化数据结构,通过在每个节点中维护多个指向其他节点的指针,形成多层索引结构,从而将查找、插入、删除操作的时间复杂度优化到 O (logn)。
1 | Level 3: 1 --------------------------> 7 |
优势:
- 实现简单(相比红黑树);
- 支持范围查询(如
subMap
、headMap
); - 插入 / 删除操作无需调整整棵树,并发性能更优。
ConcurrentSkipListMap 核心特性
与 ConcurrentHashMap 的对比
特性 | ConcurrentHashMap | ConcurrentSkipListMap |
---|---|---|
排序 | 不支持 | 支持(按 Key 自然顺序或 Comparator) |
时间复杂度 | O (1)(平均) | O(logn) |
并发控制 | CAS + synchronized | CAS |
内存占用 | 较低 | 较高(每个节点需维护多层指针) |
适用场景 | 高并发下的 KV 存储 | 需排序或范围查询的场景 |
核心方法示例
1 | ConcurrentSkipListMap<Integer, String> map = new ConcurrentSkipListMap<>(); |
性能对比与选择建议
性能测试(吞吐量对比)
操作类型 | ConcurrentHashMap (JDK8) | ConcurrentSkipListMap |
---|---|---|
随机插入 | ~1.2M ops/sec | ~500K ops/sec |
随机读取 | ~2.5M ops/sec | ~1.5M ops/sec |
范围查询 | 不支持 | ~800K ops/sec |
数据说明:测试环境为 4 核 8G 虚拟机,JDK 11,单线程写入,多线程并发读取。
选择建议
场景 | 推荐容器 |
---|---|
高并发 KV 存储,无需排序 | ConcurrentHashMap |
需排序或范围查询 | ConcurrentSkipListMap |
需强一致性(读操作阻塞) | ConcurrentHashMap + 手动同步 |
小数据量,读写均衡 | Collections.synchronizedMap |
最佳实践
ConcurrentHashMap 使用技巧
批量操作优化
1 | // 批量插入(避免多次扩容) |
避免 null 值
1 | // 手动处理 null |
ConcurrentSkipListMap 注意事项
避免频繁范围查询
范围查询需遍历跳表,性能低于随机访问,建议仅在必要时使用。
内存优化
跳表层数由随机算法决定,平均为 logn 层。可通过调整随机因子(默认 0.5)减少层数,但可能影响查询性能。
v1.3.10