一句话总结
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 锁住桶的头节点。锁粒度更细,并发度更高。
中级深入
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,迭代器会尽量反映最新状态但不保证。
实战场景
场景:本地缓存 + 原子统计
面试模拟
Q:ConcurrentHashMap 的 get 操作为什么不需要加锁?
A:因为 Node 的 val 和 next 都用 volatile 修饰,保证多线程可见性。读操作直接读 volatile 变量,不需要加锁。这也是 JDK 8 相比 JDK 7 的重要优化——JDK 7 的 get 也需要加锁(Segment 的锁)。
Q:ConcurrentHashMap 扩容时,其他线程能正常读写吗?
A:能。扩容时不阻塞读,写操作如果命中正在迁移的桶,会协助迁移(helpTransfer)。多线程协作迁移通过 transferIndex 分配任务,每个线程迁移一段,效率远高于单线程扩容。