ConcurrentHashMap 是如何保证线程安全的?

2025年 阅读约 8 分钟 面试指南 · Java面试

深入解析ConcurrentHashMap的线程安全实现,从JDK7分段锁到JDK8的CAS+synchronized,分初级、中级、高级三个层次全面讲解。

一句话总结

JDK 7 ConcurrentHashMap 用分段锁(Segment + ReentrantLock,默认 16 段)→ JDK 8 改为 CAS + synchronized 锁桶头节点(锁粒度更细,读无锁)。put 流程:桶空 CAS 插入 → 桶在扩容则协助迁移 → 否则 synchronized 锁头节点插入。size() 用 baseCount + CounterCell[] 分散计数。迭代器弱一致性,不抛 ConcurrentModificationException。

初级理解

ConcurrentHashMap 是线程安全的 HashMap,用于高并发场景。JDK 7 和 JDK 8 的实现方式完全不同:

JDK 7:分段锁(Segment) — 将整个数组分成多个段(默认 16 个),每个段独立加锁(ReentrantLock),不同段之间可以并发操作。

JDK 8:CAS + synchronized — 取消了分段锁,直接用 CAS 操作数组元素,冲突时用 synchronized 锁住桶的头节点。锁粒度更细,并发度更高。

// ConcurrentHashMap 基本使用 ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(); map.put("key", 1); // 线程安全 map.putIfAbsent("key", 2); // 原子操作:不存在才放入 map.computeIfAbsent("key", k -> 3); // 原子操作:不存在才计算

中级深入

JDK 8 ConcurrentHashMap 的 put 流程:

1. 计算 hash,定位桶位置

2. 桶为空 → CAS 尝试放入,成功则返回

3. 桶正在扩容(hash == MOVED)→ 帮助扩容

4. 桶有值 → synchronized 锁住头节点,遍历链表/红黑树插入

5. 链表长度 ≥ 8 → 转为红黑树

size() 的实现:不直接加锁统计,而是通过 baseCount + CounterCell[] 分散计数,减少竞争。sumCount() 时累加所有 CounterCell。

扩容时的多线程协作:扩容时不是单线程完成,而是将数组分段,允许多个线程同时迁移不同段的元素(transferIndex 机制),提升扩容效率。

高级拓展

为什么 JDK 8 放弃了分段锁?

1. 分段锁的并发度受限于段数量(默认 16),扩容时需要锁整个段

2. JDK 8 的 synchronized 经过大量优化(偏向锁、轻量级锁、锁粗化),性能已不输 ReentrantLock

3. CAS + synchronized 的锁粒度更细(桶级别),理论上可以支持数组长度级别的并发

ConcurrentHashMap 的弱一致性:ConcurrentHashMap 的迭代器是弱一致性的(非 fail-fast),允许在遍历过程中其他线程修改 Map,迭代器会尽量反映最新状态但不保证。

// 原子复合操作 // 错误:非原子操作 if (!map.containsKey("key")) { map.put("key", 1); // 线程不安全! } // 正确:使用原子方法 map.putIfAbsent("key", 1); // 原子操作 // 正确:使用 compute map.compute("key", (k, v) -> v == null ? 1 : v + 1);
面试加分项:能说出 JDK 7 和 JDK 8 ConcurrentHashMap 的实现差异及原因,说明你对并发编程有深入理解。

实战场景

场景:本地缓存 + 原子统计

// 场景1:本地缓存(原子初始化) ConcurrentHashMap<String, Object> cache = new ConcurrentHashMap<>(); public Object getOrLoad(String key) { return cache.computeIfAbsent(key, k -> loadFromDB(k)); } // 场景2:原子计数器 ConcurrentHashMap<String, LongAdder> counters = new ConcurrentHashMap<>(); public void increment(String key) { counters.computeIfAbsent(key, k -> new LongAdder()).increment(); } // 场景3:批量操作(forEach 支持并行) map.forEach(parallelismThreshold, (k, v) -> { // 并行处理,parallelismThreshold 控制并行粒度 }); // 场景4:search/reduce(大数据量并行搜索) String result = map.search(10000, (k, v) -> { return v.equals("target") ? k : null; });

面试模拟

Q:ConcurrentHashMap 的 get 操作为什么不需要加锁?

A:因为 Node 的 val 和 next 都用 volatile 修饰,保证多线程可见性。读操作直接读 volatile 变量,不需要加锁。这也是 JDK 8 相比 JDK 7 的重要优化——JDK 7 的 get 也需要加锁(Segment 的锁)。

Q:ConcurrentHashMap 扩容时,其他线程能正常读写吗?

A:能。扩容时不阻塞读,写操作如果命中正在迁移的桶,会协助迁移(helpTransfer)。多线程协作迁移通过 transferIndex 分配任务,每个线程迁移一段,效率远高于单线程扩容。