跳到主要内容

ConcurrentHashMap

在高并发环境下,HashMap 会出现数据错乱、死循环、覆盖写等问题。ConcurrentHashMap 等并发 Map 通过更精细的同步机制保证线程安全,是缓存、配置、计数器等场景的首选。


ConcurrentHashMap 核心机制 🧠

  • 结构Node[] table + 链表/红黑树,与 HashMap 类似,但节点字段使用 volatile
  • 桶级同步:在桶头节点上使用 synchronized(JDK8),只锁冲突链表,降低竞争。
  • CAS 插入:桶为空时使用 CAS 初始化节点,无需加锁。
  • 红黑树化:同一桶超过 8 个节点且表大小 ≥ 64 自动树化。
  • 扩容协作:触发扩容时创建 ForwardingNode,其他线程可以帮助迁移,提升效率。
  • 弱一致性迭代forEachentrySet().iterator() 不抛 ConcurrentModificationException
  • 可见性保证:桶数组与 Node.value 使用 volatile,写线程在释放锁前写入,读线程可立即看到。
  • 批量操作forEachKeys/Values/Entriesreduce 等 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");

性能对比 ⚡

特性HashMapConcurrentHashMapConcurrentSkipListMap
线程安全
顺序有序(可自定义 Comparator)
查找复杂度O(1)O(1)O(log n)
迭代一致性Fail-Fast弱一致弱一致
内存占用较低略高(额外字段+锁)较高(多层索引)
适用场景单线程或外部同步高频读写缓存、计数器有序映射、范围扫描

高频面试题 🎯

  1. ConcurrentHashMap 如何保证线程安全?
    通过“读无锁 + 写细粒度锁 + CAS + volatile”组合:读操作直接访问 volatile 数组;插入时先 CAS 初始化桶;冲突发生时只锁冲突链;扩容阶段其他线程可协助迁移。

  2. JDK7 和 JDK8 实现差异?

    • JDK7 使用 Segment 分段锁,继承 ReentrantLock
    • JDK8 移除段,直接在桶上加锁 + CAS,结构更简洁,减少内存占用。
    • JDK8 引入红黑树,避免极端情况下退化。
  3. 为什么迭代是弱一致性的?
    迭代器不获取全局锁,只保证遍历过程中不会抛异常;在遍历期间有新写入也不会立刻可见,属于“尽力而为”的快照。

  4. ConcurrentHashMap 支持 null key/value 吗?
    不支持;避免空指针语义不明确,且可以通过 map.containsKey() 明确区分“缺值”和“值为 null”。

  5. ConcurrentSkipListMap 与 TreeMap 有何不同?
    跳表是多层索引结构,部分操作可 lock-free;TreeMap 基于红黑树且非线程安全。CSLM 提供更高吞吐与并发安全,同时保持 O(log n) 时间复杂度。

  6. ConcurrentHashMap 的扩容是否会阻塞?
    不会完全阻塞:触发扩容时会设置 transferIndex,多个线程可以同时搬迁不同区间的数据,极大缩短扩容时间。

  7. 如何实现基于 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
  • 充分利用 putIfAbsentcomputeIfAbsentmerge 等原子方法,可以减少显式加锁和竞态条件。