Skip to content

概念卡片:ConcurrentHashMap 演进

一句话机制

ConcurrentHashMap 是线程安全的 HashMap,演进主线是「锁粒度变细」:1.7 用 Segment 分段锁(默认 16 段,锁住一段),1.8 改为「CAS + 局部 synchronized」(锁单个 Node 桶)+ 红黑树 + 多线程扩容。 它是「锁/CAS 知识在容器上的落地」,也是并发集合的面试核心。

1.7 → 1.8 对比

维度JDK1.7JDK1.8
数据结构Segment 数组(每段是带锁的 HashEntry 数组)数组 + 链表 + 红黑树(同 HashMap)
加锁机制Segment 分段锁(继承 ReentrantLock)CAS + synchronized(锁 Node)
锁粒度段(一段一个锁,默认 16 段)单个桶(Node)
查询两次 hash(先定位 Segment 再定位桶)get 无锁(volatile + Unsafe)
扩容段内 rehash多线程协同扩容(ForwardingNode 标记)
计数遍历 Segment 求和(可能全加锁)counterCells 分散热点(同 LongAdder)

1.7 分段锁核心

  • Segment:继承 ReentrantLock,每个 Segment 是一个独立的 HashEntry 数组
  • 默认并发度 16:最多 16 个线程同时写不同段
  • get:两次 hash + Unsafe 直接取,无锁
  • size:先不加锁循环统计,连续 2 次相等才返回;超限则强制锁全部 Segment 统计

局限:锁粒度在「段」,同一段内的写仍然串行;且段数固定(构造时指定),无法随扩容动态增加。

1.8 核心机制

CAS + synchronized(锁 Node)

java
// 桶为空 → CAS 直接插入,无锁
// 桶非空 → synchronized 锁住桶头节点,再遍历插入
  • 读(get)完全无锁:Node 数组 volatile 修饰保证数组引用可见,元素用 Unsafe/CAS 保证可见性
  • 写(put):空桶 CAS 插入,非空桶锁 Node 头节点,锁粒度最小

sizeCtl(控制初始化/扩容)

sizeCtl 值含义
0数组未初始化
-1正在初始化(单线程 CAS 抢占)
-n有 n-1 个线程正在帮助扩容
正数下一个扩容阈值

多线程协同扩容

  • ForwardingNode:hash = -1 的标记节点,扩容时放在桶上,其他线程 put 发现它就去帮助迁移数据(helpTransfer)
  • 解决「单线程扩容太慢」的问题,多核机器能并行扩容

counterCells 计数

类比 LongAdder:不竞争单个 size 变量,而是把不同线程分散到不同 CounterCell 上计数,size() 时求和。避免高并发下 ++size 的竞争。

与 HashMap 的关系

CHM 1.8 的底层结构与 HashMap 1.8 一致(数组+链表+红黑树),差异在于并发控制:HashMap 无锁(线程不安全),CHM 用 CAS + synchronized 保证安全,代价是内存多占(sizeCtl/counterCells/ForwardingNode 等额外字段)。

常见误解(避坑)

  1. ❌ "CHM 的 get 也要加锁"。→ 1.8 的 get 完全无锁,靠 volatile + Unsafe 保证可见性,读性能极高。
  2. ❌ "CHM 1.7 分段锁粒度就是最优了"。→ 1.8 用 CAS + synchronized 把锁粒度从「段」缩到「单个桶」,并发度更高。
  3. ❌ "CHM 不允许 null key/value"。→ 是的,key 和 value 都不能为 null(HashMap 允许),因为并发下无法区分「不存在」和「值为 null」。
  4. ❌ "CHM 扩容是单线程的"。→ 1.8 通过 ForwardingNode 实现多线程协同扩容,put 遇到扩容中的桶会帮忙迁移。

关联

最近更新