ConcurrentHashMap
是 Java 中一个用于高并发环境下的线程安全哈希表实现 ,它的设计核心是提高并发性能 ,避免像 Hashtable
那样对整个表加锁的做法。从源码角度来看,ConcurrentHashMap
在 Java 7 和 Java 8+ 的实现有显著差异,以 Java 8 为主。
JDK 7(分段锁 Segment)
⚠️ Java 7 使用
Segment
分段锁,每个 Segment 相当于一个小的 Hashtable,多个线程可以并发访问不同 Segment。
结构图:
java
ConcurrentHashMap
-> Segment[]
-> HashEntry[]
关键点:
Segment
继承ReentrantLock
,每个 Segment 对应一个锁。- 整个哈希表被分为多个 Segment(默认 16 个),每个 Segment 管理自己的
HashEntry
链表。 - 多线程并发时,只要访问的 Segment 不同,就不会锁冲突,提高并发性。
JDK 8+
✅ Java 8 重构了 ConcurrentHashMap
,去掉了 Segment 分段锁结构,改用节点级别的锁(synchronized + CAS)。
核心数据结构
Node 节点
java
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val; // volatile 保证可见性
volatile Node<K,V> next; // volatile 保证可见性
}
- 基础存储单元,与
HashMap
类似,但val
和next
用volatile
修饰,确保多线程可见性。
特殊节点类型
ForwardingNode
:扩容时标记迁移完成的桶(hash = -1
)。TreeBin
:红黑树的根节点(桶结构为树时使用)。ReservationNode
:占位节点,用于computeIfAbsent
等原子方法。
关键属性
java
transient volatile Node<K,V>[] table; // 主数组
private transient volatile int sizeCtl; // 控制状态
sizeCtl
是关键控制变量:0
:默认值为0,还未初始化-1
:表示正在初始化。-N
:表示有N-1
个线程正在扩容。- 正数:下一次扩容的阈值(容量 * 负载因子)。
get --- 无锁读取
java
public V get(Object key) {
Node<K,V>[] tab;
Node<K,V> e, p;
int n, eh;
K ek;
int h = spread(key.hashCode()); // 扰动函数,减少哈希冲突
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) { // table被初始化,并且长度不为0,通过 (n - 1) & h 定位当前key应在的哈希桶位置
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val; // 命中头节点
} else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null; // 特殊节点 find 查找
while ((e = e.next) != null) { // 链表查找
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
- 无锁读 :依赖
volatile
保证可见性。 - 遇到
TreeBin
或ForwardingNode
或ReservationNode
时调用其find()
方法查找。 tabAt(...)
是一个用Unsafe
类操作的数组读取方法,具有原子性,保证线程安全。
扰动函数
java
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
-
相比于HashMap,ConcurrenHashMap 使用了
& HASH_BITS
,保证扰动函数的返回为正数 -
HashMap 是通过
&(table.length - 1)
来取索引的,不关心正负数的问题。 -
在 ConcurrentHashMap 中,负哈希值有特殊含义:
javastatic final int MOVED = -1; // hash for forwarding nodes static final int TREEBIN = -2; // hash for roots of trees static final int RESERVED = -3; // hash for transient reservations
哈希值 含义 节点类型 -1
桶正在迁移 ForwardingNode
-2
红黑树根节点 TreeBin
-3
代表当前索引位置已经被占用,但是值还没有放进去 ReservationNode
put --- synchronized + CAS 插入
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; K fk; V fv;
if (tab == null || (n = tab.length) == 0)
tab = initTable(); // 初始化
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value))) // 当前桶为空,尝试cas放入新节点
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f); // hash冲突,正在扩容,协助扩容
else if (onlyIfAbsent // check first node without acquiring lock
&& fh == hash
&& ((fk = f.key) == key || (fk != null && key.equals(fk)))
&& (fv = f.val) != null)
return fv; // 相同的key且onlyIfAbsent,直接返回原值,不更新
else {
V oldVal = null;
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);
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;
}
}
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i); // 尝试树化,判断条件:桶元素>8 && 总元素>64
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
transfer 扩容
- 元素总数超过
sizeCtl
时触发扩容。
java
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
// 步骤1:分配迁移任务区间(stride)
// 步骤2:每个线程负责迁移一段桶
while (advance) {
// 领取一个桶的迁移任务
}
// 步骤3:迁移数据(链表/树拆分为高位桶和低位桶)
synchronized (f) {
// 迁移操作
}
// 步骤4:放置 ForwardingNode 标记迁移完成,在synchronized中操作
}
- 线程通过
transferIndex
领取迁移任务区间。每个线程最少负责16个桶的迁移工作。 - 迁移时对桶加锁,避免并发问题。
- 迁移完成后用
ForwardingNode
标记,读请求可转发到新表。