ConcurrentHashMap
在高并发环境下,HashMap 会出现数据错乱、死循环、覆盖写等问题。ConcurrentHashMap 等并发 Map 通过更精细的同步机制保证线程安全,是缓存、配置、计数器等场景的首选。
ConcurrentHashMap 核心机制 🧠
- 结构:
Node[] table+ 链表/红黑树,与 HashMap 类似,但节点字段使用volatile。 - 桶级同步:在桶头节点上使用
synchronized(JDK8),只锁冲突链表,降低竞争。 - CAS 插入:桶为空时使用
CAS初始化节点,无需加锁。 - 红黑树化:同一桶超过 8 个节点且表大小 ≥ 64 自动树化。
- 扩容协作:触发扩容时创建
ForwardingNode,其他线程可以帮助迁移,提升效率。 - 弱一致性迭代:
forEach、entrySet().iterator()不抛ConcurrentModificationException。 - 可见性保证:桶数组与
Node.value使用volatile,写线程在释放锁前写入,读线程可立即看到。 - 批量操作:
forEachKeys/Values/Entries、reduce等 API 允许指定并行阈值,内部借助ForkJoinPool.commonPool()。
常用 API 🔧
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("Java", 1);
map.putIfAbsent("Java", 2); // 原子性插入
map.compute("Java", (k, v) -> v == null ? 1 : v + 1);
map.computeIfAbsent("Python", key -> expensiveLoad(key));
map.merge("Go", 1, Integer::sum); // 原子累加
map.forEach(2, (k, v) -> System.out.println(k + "=" + v)); // 并行度 2
// 批量操作
map.reduceValues(4, Integer::sum); // 并行汇总 value
ConcurrentSkipListMap 🌉
ConcurrentSkipListMap 基于跳表,天然有序、支持范围查询,且读写都具备无锁或细粒度锁特性。
- 有序性:按照 key 自然顺序或
Comparator排序。 - 复杂度:查找、插入、删除均为 O(log n)。
- 并发:通过
CAS与轻量级锁实现多层指针更新。 - 适用场景:需要排序视图、范围查询、基于时间窗口的任务调度。
ConcurrentSkipListMap<Long, String> timeline = new ConcurrentSkipListMap<>();
timeline.put(System.currentTimeMillis(), "event");
timeline.headMap(System.currentTimeMillis()); // 取指定时间前的所有事件
timeline.ceilingEntry(target); // >= target 的最小键
并发 Set 的 Map 实现 🔐
ConcurrentHashMap.newKeySet():基于 CHM 的高性能 set。Collections.newSetFromMap(new ConcurrentHashMap<>()):通用方式。ConcurrentSkipListSet:基于跳表,天然有序。
Set<String> onlineUsers = ConcurrentHashMap.newKeySet();
onlineUsers.add("alice");
onlineUsers.contains("bob");
性能对比 ⚡
| 特性 | HashMap | ConcurrentHashMap | ConcurrentSkipListMap |
|---|---|---|---|
| 线程安全 | ❌ | ✅ | ✅ |
| 顺序 | 无 | 无 | 有序(可自定义 Comparator) |
| 查找复杂度 | O(1) | O(1) | O(log n) |
| 迭代一致性 | Fail-Fast | 弱一致 | 弱一致 |
| 内存占用 | 较低 | 略高(额外字段+锁) | 较高(多层索引) |
| 适用场景 | 单线程或外部同步 | 高频读写缓存、计数器 | 有序映射、范围扫描 |
高频面试题 🎯
-
ConcurrentHashMap 如何保证线程安全?
通过“读无锁 + 写细粒度锁 + CAS + volatile”组合:读操作直接访问volatile数组;插入时先 CAS 初始化桶;冲突发生时只锁冲突链;扩容阶段其他线程可协助迁移。 -
JDK7 和 JDK8 实现差异?
- JDK7 使用
Segment分段锁,继承ReentrantLock。 - JDK8 移除段,直接在桶上加锁 + CAS,结构更简洁,减少内存占用。
- JDK8 引入红黑树,避免极端情况下退化。
- JDK7 使用
-
为什么迭代是弱一致性的?
迭代器不获取全局锁,只保证遍历过程中不会抛异常;在遍历期间有新写入也不会立刻可见,属于“尽力而为”的快照。 -
ConcurrentHashMap 支持 null key/value 吗?
不支持;避免空指针语义不明确,且可以通过map.containsKey()明确区分“缺值”和“值为 null”。 -
ConcurrentSkipListMap 与 TreeMap 有何不同?
跳表是多层索引结构,部分操作可 lock-free;TreeMap 基于红黑树且非线程安全。CSLM 提供更高吞吐与并发安全,同时保持 O(log n) 时间复杂度。 -
ConcurrentHashMap 的扩容是否会阻塞?
不会完全阻塞:触发扩容时会设置transferIndex,多个线程可以同时搬迁不同区间的数据,极大缩短扩容时间。 -
如何实现基于 ConcurrentHashMap 的计数器?
使用LongAdder作为 value:stats.computeIfAbsent(key, k -> new LongAdder()).increment();,避免热点 key 上的 CAS 竞争。
场景推荐 ✅
- 统计/缓存:
ConcurrentHashMap+LongAdder/AtomicInteger - 排序缓存:
ConcurrentSkipListMap管理有序数据(如延迟任务、K 线数据) - 在线用户集合:
ConcurrentHashMap.newKeySet() - 广播订阅:
CopyOnWriteArraySet存监听器,配合 CHM 存订阅关系
小结
- 并发 Map 是 JUC 中使用频率最高的组件,务必熟悉其 API 与底层策略。
ConcurrentHashMap适合绝大多数非排序场景;如需有序视图或范围查询,则选择ConcurrentSkipListMap/Set。- 充分利用
putIfAbsent、computeIfAbsent、merge等原子方法,可以减少显式加锁和竞态条件。