Java8 ConcurrentHashMap 深度解析(底层数据结构详解及方法执行流程)
一、数据结构 (Data Structure)
Java 8 的 ConcurrentHashMap 摒弃了 Java 7 中的 Segment 分段锁 机制,采用了与 HashMap 1.8 类似的 数组 + 链表 + 红黑树 的结构,但在并发控制上做了特殊设计。
1. 核心结构图
ConcurrentHashMap ├── Node[] table (volatile) // 哈希桶数组 │ ├── Node (链表节点) │ ├── TreeBin (红黑树节点包装) │ └── ForwardingNode (扩容迁移标志) ├── Node[] nextTable // 扩容时的新数组 ├── LongAdder baseCount // 基础计数 └── CounterCell[] counterCells // 并发计数单元格 (解决热点竞争)2. 关键节点类型
- Node: 存储键值对的基本节点,包含
key,val,hash,next。 - TreeBin: 当链表长度超过阈值(默认 8)且数组长度超过 64 时,链表转为红黑树。
TreeBin是红黑树的根节点包装,不直接存储数据,而是维护树的平衡。 - ForwardingNode: 专门用于扩容。当某个桶正在被迁移时,该位置会放置一个
ForwardingNode,其hash值为MOVED (-1),指向新的数组nextTable,引导后续请求去新数组操作。
3. 并发控制核心
- CAS (Compare-And-Swap): 用于无锁插入节点(如数组初始化、空桶插入)。
- synchronized: 当发生哈希冲突(链表或树操作)时,只锁定当前桶的 头节点 (Head Node)。粒度更细,性能优于 Java 7 的 Segment 锁。
- volatile:
table数组和Node的val、next指针都是 volatile 的,保证可见性。
二、put() 方法执行流程
putVal 是 put 的核心实现,流程较为复杂,主要步骤如下:
1. 参数检查与 Hash 计算
- 检查 key 和 value 是否为 null(CHM 不允许 null)。
- 计算 key 的 hash 值(使用扰动函数,高位参与运算,减少冲突)。
2. 数组初始化 (initTable)
- 如果
table为 null 或长度为 0,调用initTable()。 - 使用 CAS 竞争初始化权。如果当前线程发现
sizeCtl < 0,说明其他线程正在初始化,当前线程让出 CPU (Thread.yield()) 自旋等待。 - 初始化成功后,设置
sizeCtl为扩容阈值(通常为容量的 0.75)。
3. 定位桶位置
- 根据
(n - 1) & hash计算数组下标i。 - 获取该位置的节点
f。
4. 插入逻辑 (核心分支)
这里分为三种情况:
- 情况 A:桶为空 (f == null)
- 使用 CAS 尝试将新节点插入到该位置。
- 如果 CAS 成功,直接结束。
- 如果 CAS 失败(说明有其他线程竞争),进入自旋重试。
- 情况 B:正在扩容 (f.hash == MOVED)
- 说明当前桶的节点是
ForwardingNode。 - 调用
helpTransfer(),当前线程会 协助扩容,将旧数组的数据迁移到新数组nextTable中。
- 说明当前桶的节点是
- 情况 C:桶不为空且未扩容 (哈希冲突)
- 使用
synchronized (f)锁定该桶的头节点。 - 再次检查 头节点是否变化(双重检查)。
- 链表插入: 遍历链表。
- 如果找到相同 key,根据
onlyIfAbsent决定是否覆盖 value。 - 如果没找到,尾插法插入新节点。
- 插入后检查链表长度,如果 >= 8,调用
treeifyBin尝试转为红黑树(需数组长度 >= 64,否则先扩容)。
- 如果找到相同 key,根据
- 红黑树插入: 如果
f是TreeBin类型,调用putTreeVal插入树节点。 - 解锁。
- 使用
5. 计数与扩容检查
- 调用
addCount(1L, binCount)更新元素个数。 - 检查是否达到扩容阈值,如果达到,触发
transfer()进行扩容(容量翻倍)。
public V put(K key, V value) { return putVal(key, value, false); } /** Implementation for put and putIfAbsent */ final V putVal(K key, V value, boolean onlyIfAbsent) { // key 和 value 不能为空 if (key == null || value == null) throw new NullPointerException(); int hash = spread(key.hashCode()); int binCount = 0; for (Node<K,V>[] tab = table;;) { // f = 目标位置元素 Node<K,V> f; int n, i, fh;// fh 后面存放目标位置的元素 hash 值 if (tab == null || (n = tab.length) == 0) // 数组桶为空,初始化数组桶(自旋+CAS) tab = initTable(); else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 桶内为空,CAS 放入,不加锁,成功了就直接 break 跳出 if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null))) break; // no lock when adding to empty bin } else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); else { V oldVal = null; // 使用 synchronized 加锁加入节点 synchronized (f) { if (tabAt(tab, i) == f) { // 说明是链表 if (fh >= 0) { binCount = 1; // 循环加入新的或者覆盖节点 for (Node<K,V> e = f;; ++binCount) { K ek; if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { oldVal = e.val; if (!onlyIfAbsent) e.val = value; break; } Node<K,V> pred = e; if ((e = e.next) == null) { pred.next = new Node<K,V>(hash, key, value, null); break; } } } else if (f instanceof TreeBin) { // 红黑树 Node<K,V> p; binCount = 2; if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } } } } if (binCount != 0) { if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } addCount(1L, binCount); return null; }三、get() 方法执行流程
get 操作是 无锁 的,这是 CHM 高性能的关键之一。
1. 计算 Hash 与定位
- 计算 key 的 hash 值。
- 定位数组下标
i = (n - 1) & hash。 - 获取该位置的第一个节点
tab[i]。
2. 遍历查找
- 匹配头节点: 如果头节点的 hash 和 key 匹配,直接返回 value。
- 链表遍历: 如果不匹配且
next不为 null,遍历链表查找。 - 红黑树查找: 如果节点类型是
TreeBin,调用find方法在红黑树中查找。 - 扩容处理: 如果节点是
ForwardingNode,说明正在扩容,需要在nextTable(新数组) 中继续查找。
3. 返回结果
- 找到则返回 value,找不到返回 null。
- 注意: 整个过程中没有使用任何锁,依靠
volatile保证读取到最新的数据。
public V get(Object key) { Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek; // key 所在的 hash 位置 int h = spread(key.hashCode()); if ((tab = table) != null && (n = tab.length) > 0 && (e = tabAt(tab, (n - 1) & h)) != null) { // 如果指定位置元素存在,头结点hash值相同 if ((eh = e.hash) == h) { if ((ek = e.key) == key || (ek != null && key.equals(ek))) // key hash 值相等,key值相同,直接返回元素 value return e.val; } else if (eh < 0) // 头结点hash值小于0,说明正在扩容或者是红黑树,find查找 return (p = e.find(h, key)) != null ? p.val : null; while ((e = e.next) != null) { // 是链表,遍历查找 if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek)))) return e.val; } } return null; }四、size() 方法执行流程
Java 8 中 size() 的返回类型是 int,但底层统计逻辑基于 long。它不是 O(1) 的操作,而是一个估算值(在并发修改时)。
1. 计数机制 (LongAdder 思想)
为了减少高并发下对单一计数变量的竞争,CHM 采用了类似 LongAdder 的机制:
- baseCount: 基础计数值。
- CounterCell[]: 一个数组,当并发竞争激烈时,线程会将计数累加到不同的
CounterCell单元格中,最后求和。
2. 执行流程 (sumCount())
- 读取
baseCount。 - 遍历
counterCells数组,累加所有非空单元格的值。 - 总和 =
baseCount+counterCells之和。
3. 返回值
size()方法内部调用sumCount(),如果结果超过Integer.MAX_VALUE则返回最大值,否则强转为int。- 注意: 由于并发修改,
size()返回的值可能不是强一致性的(即调用瞬间的真实大小),但在大多数场景下足够准确。如果需要强一致性大小,需要加锁统计,但性能会大幅下降。
五. put 元素时如何更新计数?
下文来自ConcurrentHashMap 源码分析 | JavaGuide


六、核心优化点总结 (面试加分项)
- 锁粒度优化: 从 Java 7 的 Segment 分段锁(锁整个段)变为 Java 8 的 桶级别锁 (
synchronized)。JVM 对synchronized做了大量优化(偏向锁、轻量级锁),在低冲突下性能极高。 - CAS 无锁插入: 如果桶为空,直接 CAS 插入,无需加锁。
- 红黑树优化: 当哈希冲突严重时,链表转为红黑树,查找时间复杂度从 O(n) 降为 O(log n)。
- 协同扩容: 扩容时,多个线程可以一起帮助迁移数据 (
helpTransfer),加快了扩容速度,避免了单线程迁移导致的停顿。 - 读写分离:
get操作完全无锁,put操作仅在冲突时加锁,极大提高了读多写少场景的性能。