ConcurrentHashMap 源码分析
ConcurrentHashMap 是 Java 并发包(java.util.concurrent)提供的线程安全、高效的哈希表实现,旨在替代线程安全但性能低下的 Hashtable(全局锁)和非线程安全的 HashMap。其核心设计目标是在保证线程安全的前提下,最大化并发访问效率。
本文将按 JDK 1.7 → JDK 1.8 的演进逻辑,从数据结构、核心方法、锁机制、性能优化等角度深入分析源码,揭示其设计思想。
一、核心特性总览
- 线程安全:通过分段锁(1.7)或 CAS + synchronized(1.8)实现,避免全局锁导致的并发瓶颈;
- 高效并发:支持多线程同时读写不同桶(或分段),仅对冲突节点加锁,锁粒度极小;
- 弱一致性迭代 :迭代器不抛出
ConcurrentModificationException,遍历的是当前数据的 "快照"; - 不支持 null 键 / 值:避免并发场景下无法区分 "key 不存在" 和 "value 为 null";
- 支持原子操作 :如
putIfAbsent()、compute()等,无需额外加锁。
二、JDK 1.7 实现:分段锁(Segment + HashEntry)
JDK 1.7 的 ConcurrentHashMap 核心是分段锁(Segment Lock) 机制,将哈希表拆分为多个独立的 "分段(Segment)",每个分段本质是一个独立的哈希表(ReentrantLock + HashEntry 数组 + 链表)。
1. 数据结构
java
// ConcurrentHashMap 核心类(JDK 1.7)
public class ConcurrentHashMap<K, V> {
// 分段数组(每个 Segment 是一个独立的锁和哈希表)
final Segment<K, V>[] segments;
// 每个分段的默认容量
static final int DEFAULT_INITIAL_CAPACITY = 16;
// 分段数(默认 16,必须是 2 的幂,并发度默认 16)
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
// 负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 分段类(继承 ReentrantLock,自带锁能力)
static final class Segment<K, V> extends ReentrantLock implements Serializable {
// 每个分段内部的 HashEntry 数组(链表数组)
transient volatile HashEntry<K, V>[] table;
// 分段内元素个数
transient int count;
// 扩容阈值(count >= threshold 时扩容)
transient int threshold;
// 负载因子
final float loadFactor;
}
// 哈希节点(链表节点)
static final class HashEntry<K, V> {
final int hash;
final K key;
// value 用 volatile 保证可见性
volatile V value;
// next 用 volatile 保证链表修改的可见性
volatile HashEntry<K, V> next;
}
}
核心设计:
- 分段锁隔离:每个 Segment 是一个独立的 ReentrantLock,线程操作某分段时,仅锁定该分段,其他分段可并发访问(并发度 = 分段数,默认 16);
- volatile 保证可见性:HashEntry 的 value 和 next 用 volatile 修饰,避免线程间数据不可见;
- 分段独立扩容:每个分段单独计算扩容阈值,扩容时仅锁定当前分段,不影响其他分段。
2. 核心方法分析
(1)put 方法:分段加锁插入
java
public V put(K key, V value) {
if (value == null) throw new NullPointerException(); // 不允许 null 值
int hash = hash(key);
// 计算 key 所属的分段索引(segments 数组的下标)
int segmentIndex = (hash >>> segmentShift) & segmentMask;
// 获取目标分段,若未初始化则初始化
Segment<K, V> s = segmentFor(segmentIndex);
// 分段内插入节点(Segment 类的 put 方法)
return s.put(key, hash, value, false);
}
// Segment 类的 put 方法(核心逻辑)
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 尝试获取锁,获取失败则进入自旋(非阻塞获取锁,提升性能)
HashEntry<K, V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K, V>[] tab = table;
// 计算 key 在分段内的桶索引
int index = (tab.length - 1) & hash;
HashEntry<K, V> first = entryAt(tab, index);
for (HashEntry<K, V> e = first; ; ) {
if (e != null) {
// key 已存在,更新 value
K k;
if ((k = e.key) == key || (e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value; // volatile 写,保证可见性
++modCount;
}
break;
}
e = e.next;
} else {
// key 不存在,插入新节点(链表头插法)
if (node != null)
node.setNext(first);
else
node = new HashEntry<>(hash, key, value, first);
int c = count + 1;
// 检查是否需要扩容
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node); // 分段内扩容
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock(); // 释放锁
}
return oldValue;
}
关键逻辑:
- 计算 key 的哈希值,定位到所属分段;
- 尝试获取分段锁(
tryLock()),失败则自旋重试(避免阻塞); - 分段内定位桶索引,遍历链表:
- 若 key 已存在,更新 value(volatile 写保证可见性);
- 若 key 不存在,插入新节点(头插法);
- 检查分段内元素个数是否超过阈值,若超过则分段独立扩容;
- 释放锁。
(2)get 方法:无锁访问
java
public V get(Object key) {
int hash = hash(key);
// 定位分段和桶索引
return segmentFor(hash).get(key, hash);
}
// Segment 类的 get 方法
final V get(Object key, int hash) {
if (count != 0) { // 分段内有元素才遍历
HashEntry<K, V> e = getFirst(hash);
while (e != null) {
if (e.hash == hash && key.equals(e.key)) {
V v = e.value;
if (v != null)
return v; // value 是 volatile,保证可见性
// 若 value 为 null,可能是节点正在被删除,自旋重试
return readValueUnderLock(e);
}
e = e.next; // next 是 volatile,保证链表遍历的可见性
}
}
return null;
}
关键设计:
- 无锁读取:get 方法无需加锁,依赖 HashEntry 的 value 和 next 是 volatile 修饰,保证线程间可见性;
- 自旋重试:若 value 为 null(可能是节点正在被删除),则加锁后重新读取,避免读取中间状态。
3. JDK 1.7 缺点
- 并发度有限:并发度 = 分段数(默认 16),若分段数固定,高并发下仍可能出现分段锁竞争;
- 扩容效率低:分段独立扩容,但若某分段元素过多,扩容时会独占该分段锁,影响性能;
- 链表查询效率低:无红黑树优化,哈希冲突严重时,链表查询时间复杂度为 O (n)。
三、JDK 1.8 实现:CAS + synchronized + 红黑树
JDK 1.8 对 ConcurrentHashMap 进行了彻底重构 ,放弃了分段锁,采用「数组 + 链表 + 红黑树」的结构(与 HashMap 一致),通过 CAS 无锁操作 + synchronized 细粒度锁 保证线程安全,同时引入红黑树优化哈希冲突。
1. 数据结构
java
public class ConcurrentHashMap<K, V> {
// 哈希表数组(volatile 修饰,保证数组扩容/初始化的可见性)
transient volatile Node<K, V>[] table;
// 扩容时的临时数组(transfer 期间使用)
private transient volatile Node<K, V>[] nextTable;
// 基础计数器(无竞争时使用)
private transient volatile long baseCount;
// 控制标志位(-1:正在初始化,-N:N-1 个线程正在扩容,正数:下次扩容阈值)
private transient volatile int sizeCtl;
// 核心节点(链表节点)
static class Node<K, V> implements Map.Entry<K, V> {
final int hash;
final K key;
// value 和 next 用 volatile 保证可见性
volatile V val;
volatile Node<K, V> next;
}
// 红黑树节点(链表长度 >= 8 时转为红黑树)
static final class TreeNode<K, V> extends Node<K, V> {
TreeNode<K, V> parent; // 父节点
TreeNode<K, V> left; // 左子节点
TreeNode<K, V> right; // 右子节点
TreeNode<K, V> prev; // 链表前驱(用于红黑树转链表)
boolean red; // 红黑树颜色标记
}
// 扩容标记节点(ForwardingNode 所在的桶表示已迁移完成)
static final class ForwardingNode<K, V> extends Node<K, V> {
final Node<K, V>[] nextTable; // 指向新数组
ForwardingNode(Node<K, V>[] tab) {
super(MOVED, null, null, null); // hash = MOVED(-1)
this.nextTable = tab;
}
}
}
核心设计:
- 锁粒度细化 :放弃分段锁,直接对哈希桶的头节点加 synchronized 锁,仅阻塞同一桶的并发操作,不同桶完全并行;
- CAS 无锁优化:初始化数组、插入首节点等场景用 CAS 操作,避免加锁;
- 红黑树优化:链表长度 >= 8 时转为红黑树(查询时间复杂度从 O (n) 降至 O (log n));
- 协助扩容:扩容时,其他线程访问到 ForwardingNode 会主动参与扩容,提升效率;
- 原子计数:用 baseCount + CounterCell 数组实现原子计数,避免全局锁。
2. 核心方法分析
(1)initTable:初始化哈希表(CAS 无锁)
java
private final Node<K, V>[] initTable() {
Node<K, V>[] tab; int sc;
// 循环 CAS 初始化,避免并发冲突
while ((tab = table) == null || tab.length == 0) {
// sizeCtl < 0 表示其他线程正在初始化,当前线程让出 CPU
if ((sc = sizeCtl) < 0)
Thread.yield();
// CAS 将 sizeCtl 设为 -1(标记正在初始化)
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
// 初始化数组容量(默认 16)
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K, V>[] nt = (Node<K, V>[])new Node<?, ?>[n];
table = tab = nt;
// 计算扩容阈值(n * 0.75)
sc = n - (n >>> 2);
}
} finally {
// 更新 sizeCtl 为扩容阈值
sizeCtl = sc;
}
break;
}
}
return tab;
}
关键逻辑:
- 用
sizeCtl控制初始化状态:sc > 0表示未初始化(sc 为容量),sc = -1表示正在初始化; - 并发初始化时,通过 CAS 竞争
sizeCtl的修改权,失败的线程 yield 让出 CPU,避免忙等。
(2)putVal:核心插入方法(CAS + synchronized)
java
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode()); // 二次哈希,减少冲突
int binCount = 0;
for (Node<K, V>[] tab = table;;) {
Node<K, V> f; int n, i, fh;
// 哈希表未初始化,先初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// 桶位为空,CAS 插入首节点(无锁操作)
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<>(hash, key, value, null)))
break; // CAS 成功则退出循环
}
// 桶位是 ForwardingNode(当前桶正在扩容),协助扩容
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) { // 链表节点(hash >= 0)
binCount = 1;
for (Node<K, V> e = f;; ++binCount) {
K ek;
// key 已存在,更新 value
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value; // volatile 写
break;
}
Node<K, V> pred = e;
// 遍历到链表尾部,插入新节点(尾插法)
if ((e = e.next) == null) {
pred.next = new Node<>(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;
}
}
}
}
// 检查是否需要将链表转为红黑树(binCount >= 8)
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i); // 链表转红黑树
if (oldVal != null)
return oldVal;
break;
}
}
}
// 统计元素个数(原子操作)
addCount(1L, binCount);
return null;
}
关键逻辑拆解:
- 二次哈希 :
spread(hashCode)将 hashCode 高位与低位混合,减少哈希冲突; - 初始化检查 :若哈希表未初始化,调用
initTable()初始化; - 无锁插入首节点 :桶位为空时,用
casTabAt()(CAS 操作)插入节点,避免加锁; - 协助扩容 :若桶位是 ForwardingNode(hash = MOVED),调用
helpTransfer()参与扩容; - 细粒度锁:对桶的头节点加 synchronized 锁,保证同一桶的并发安全;
- 链表 / 红黑树插入:
- 链表节点(hash >= 0):遍历链表,存在则更新,不存在则尾插;
- 红黑树节点(TreeBin):调用红黑树插入逻辑;
- 链表转红黑树 :链表长度 >= 8 时,调用
treeifyBin()转为红黑树; - 原子计数 :调用
addCount()更新元素个数。
(3)get 方法:无锁读取(volatile 保证可见性)
java
public V get(Object key) {
Node<K, V>[] tab; Node<K, V> e, p; int n, eh; K ek;
int hash = spread(key.hashCode());
// 哈希表不为空,且桶位存在节点
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & hash)) != null) {
// 头节点就是目标节点,直接返回
if ((eh = e.hash) == hash) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// 红黑树节点(eh < 0 且 e 是 TreeBin)
else if (eh < 0)
return (p = e.find(hash, key)) != null ? p.val : null;
// 链表节点,遍历查找
while ((e = e.next) != null) {
if (e.hash == hash &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
关键设计:
- 无锁读取:依赖 Node 的 val 和 next 是 volatile 修饰,保证线程间可见性;
- 快速定位:
- 头节点匹配直接返回;
- 红黑树节点调用
find()方法查找(O (log n)); - 链表节点遍历查找(O (n),但红黑树优化后概率极低)。
(4)transfer:扩容方法(多线程协助扩容)
扩容是 ConcurrentHashMap 1.8 的核心优化之一,支持多线程同时扩容,流程如下:
- 触发条件:元素个数 >= sizeCtl(扩容阈值);
- 初始化新数组:新数组容量 = 原数组容量 * 2;
- 标记扩容状态 :将 sizeCtl 设为
-N(N 为参与扩容的线程数); - 迁移桶数据:对每个桶加锁,将原数组的桶数据迁移到新数组的两个桶(因为容量翻倍,哈希值高位参与计算);
- 标记迁移完成:迁移后的桶插入 ForwardingNode,告知其他线程该桶已迁移;
- 协助扩容 :其他线程访问到 ForwardingNode 时,会调用
helpTransfer()参与迁移,直到所有桶迁移完成。
3. 原子计数:baseCount + CounterCell
JDK 1.8 用「基础计数器 + 单元格数组」实现原子计数,避免全局锁:
- 无竞争场景 :直接用 CAS 更新
baseCount; - 高竞争场景 :若 CAS 失败,创建
CounterCell数组,每个线程更新不同的 CounterCell,最后求和得到总元素个数。
java
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
// 尝试 CAS 更新 baseCount,失败则操作 CounterCell
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
// CounterCell 数组未初始化,或当前线程的 Cell CAS 失败
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
// 初始化 CounterCell 或处理竞争
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount(); // 求和 baseCount + 所有 CounterCell 的值
}
// 检查是否需要扩容
if (check >= 0) {
Node<K, V>[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
// CAS 增加参与扩容的线程数(sc += 1)
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// 标记扩容开始(sizeCtl = rs << RESIZE_STAMP_SHIFT + 2)
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
四、JDK 1.7 vs JDK 1.8 核心差异
| 对比维度 | JDK 1.7 | JDK 1.8 |
|---|---|---|
| 数据结构 | Segment 数组 + HashEntry 链表 | Node 数组 + 链表 / 红黑树 |
| 锁机制 | 分段锁(ReentrantLock) | CAS + synchronized(桶头节点锁) |
| 并发度 | 固定(默认 16,分段数) | 动态(与桶数一致,默认 16) |
| 扩容机制 | 分段独立扩容 | 多线程协助扩容 |
| 哈希冲突优化 | 仅链表(O (n)) | 链表转红黑树(O (log n)) |
| 计数方式 | 分段 count 求和(加锁) | baseCount + CounterCell(原子) |
| 锁粒度 | 粗粒度(分段) | 细粒度(桶) |
五、常见问题与总结
1. 为什么 ConcurrentHashMap 不支持 null 键 / 值?
- 并发场景下,
get(key)返回 null 无法区分 "key 不存在" 和 "value 为 null"; - HashMap 支持 null 是因为非线程安全,可通过
containsKey(key)辅助判断,但 ConcurrentHashMap 并发环境下containsKey和get之间可能存在数据修改,无法保证一致性。
2. 与 Hashtable 的区别?
- 锁机制:Hashtable 是全局锁(synchronized 修饰方法),ConcurrentHashMap 是细粒度锁(1.8);
- 性能:ConcurrentHashMap 支持高并发,Hashtable 并发性能极差;
- null 支持:Hashtable 支持 null 键 / 值,ConcurrentHashMap 不支持;
- 迭代器:Hashtable 迭代器是快速失败(抛 ConcurrentModificationException),ConcurrentHashMap 是弱一致性。
3. 核心总结
- 演进逻辑:从 "分段锁隔离" 到 "CAS + 细粒度 synchronized",锁粒度更细,并发度更高;
- 性能优化:红黑树优化哈希冲突、多线程协助扩容、无锁读取、原子计数,最大化并发效率;
- 设计思想:在保证线程安全的前提下,通过 "锁粒度最小化 + 无锁操作 + 协助机制" 提升并发性能,是 Java 并发编程中哈希表的首选实现。