ConcurrentMap接口设计分析
ConcurrentMap<K,V>
继承自 Map<K,V>
,专门为并发环境设计,提供了线程安全和原子性保证的键值对存储接口。
核心抽象方法
原子性条件操作方法
V putIfAbsent(K key, V value)
- 仅在键不存在时插入值boolean remove(Object key, Object value)
- 仅在键值匹配时删除boolean replace(K key, V oldValue, V newValue)
- 仅在旧值匹配时替换V replace(K key, V value)
- 仅在键存在时替换值
这些方法的设计核心是compare-and-swap (CAS) 语义,确保操作的原子性。
默认方法分析
弱一致的查询方法
getOrDefault(Object key, V defaultValue)
default V getOrDefault(Object key, V defaultValue) {
V v;
return ((v = get(key)) != null) ? v : defaultValue;
}
设计特点:
- 假设实现类不支持null值
- 通过
get()
返回null来明确表示键不存在 - 避免了传统Map中null值的歧义性
并发安全的遍历方法
forEach(BiConsumer<? super K, ? super V> action)
default void forEach(BiConsumer<? super K, ? super V> action) {
Objects.requireNonNull(action);
for (Map.Entry<K,V> entry : entrySet()) {
K k; V v;
try {
k = entry.getKey();
v = entry.getValue();
} catch (IllegalStateException ise) {
continue; // 条目已被删除,跳过
}
action.accept(k, v);
}
}
并发处理机制:
- 捕获
IllegalStateException
来处理并发删除的条目 - 确保遍历过程中的弱一致性语义
原子性批量更新方法
replaceAll(BiFunction<? super K, ? super V, ? extends V> function)
default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
Objects.requireNonNull(function);
forEach((k,v) -> {
while (!replace(k, v, function.apply(k, v))) {
if ((v = get(k)) == null) break; // 键已被删除
}
});
}
重试机制:
- 使用
while
循环确保CAS操作成功 - 通过
get(k) == null
检测键是否被并发删除
条件计算方法
computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
default V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
Objects.requireNonNull(mappingFunction);
V oldValue, newValue;
return ((oldValue = get(key)) == null
&& (newValue = mappingFunction.apply(key)) != null
&& (oldValue = putIfAbsent(key, newValue)) == null)
? newValue : oldValue;
}
原子性保证:
- 使用
putIfAbsent()
确保只有一个线程能成功插入值 - 短路求值避免不必要的计算
computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)
default V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
Objects.requireNonNull(remappingFunction);
for (V oldValue; (oldValue = get(key)) != null; ) {
V newValue = remappingFunction.apply(key, oldValue);
if ((newValue == null) ? remove(key, oldValue) : replace(key, oldValue, newValue))
return newValue;
}
return null;
}
循环重试机制:
- 通过
for
循环处理并发修改 - 三元运算符根据新值是否为null选择删除或替换操作
设计原则总结
1. 无锁编程模式
所有默认方法都基于CAS操作,避免使用显式锁,提高并发性能。
2. 弱一致性语义
遍历和批量操作可能看到不一致的中间状态,但保证最终一致性。
3. Null值假设
默认实现假设不支持null值,简化了并发控制的复杂性。
4. 重试机制
通过循环和条件检查处理CAS失败,确保操作最终成功或明确失败。
这种设计使得 ConcurrentMap
成为高并发环境下键值对存储的理想选择,在保证线程安全的同时最大化了性能。
ConcurrentHashMap 深度分析
继承自 AbstractMap<K,V>,拥有Hash Map一致的能力,又 实现 ConcurrentMap<K,V>接口,表示有并发安全的能力。本节将深入分析 ConcurrentHashMap
的关键实现。
核心数据结构
Node结构体系
// 基础节点
static class Node<K,V> implements Map.Entry<K,V>
// 树节点
static final class TreeNode<K,V> extends Node<K,V>
// 树容器
static final class TreeBin<K,V> extends Node<K,V>
// 转发节点(扩容时使用)
static final class ForwardingNode<K,V> extends Node<K,V>
// 预留节点(计算时占位)
static final class ReservationNode<K,V> extends Node<K,V>
关键字段
transient volatile Node<K,V>[] table; // 主表
private transient volatile Node<K,V>[] nextTable; // 扩容时的新表
private transient volatile long baseCount; // 基础计数
private transient volatile int sizeCtl; // 控制字段
private transient volatile int transferIndex; // 扩容传输索引
private transient volatile CounterCell[] counterCells; // 计数单元
哈希和访问机制
哈希计算与分布
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
设计亮点:
- 高16位与低16位异或,减少碰撞
& HASH_BITS
确保最高位为0,区分特殊节点
内存访问优化
// 获取数组元素(Acquire语义)
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getReferenceAcquire(tab, ((long)i << ASHIFT) + ABASE);
}
// 设置数组元素(Release语义)
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putReferenceRelease(tab, ((long)i << ASHIFT) + ABASE, v);
}
内存模型保证:
- 使用
Unsafe
直接操作内存 - Acquire/Release 语义确保可见性
- 避免传统
volatile
数组的性能开销
HASH_BITS
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
特殊hash值常量
static final int MOVED = -1; // ForwardingNode(扩容转发节点)
static final int TREEBIN = -2; // TreeBin(红黑树根节点)
static final int RESERVED = -3; // ReservationNode(占位节点)
特殊节点类型
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); // hash = -1
this.nextTable = tab;
}
}
TreeBin - 红黑树容器节点
static final class TreeBin<K,V> extends Node<K,V> {
TreeNode<K,V> root;
volatile TreeNode<K,V> first;
TreeBin(TreeNode<K,V> b) {
super(TREEBIN, null, null); // hash = -2
// 树结构初始化...
}
}
ReservationNode - 预留占位节点
static final class ReservationNode<K,V> extends Node<K,V> {
ReservationNode() {
super(RESERVED, null, null); // hash = -3
}
}
方法中特殊处理
get()方法中的特殊处理
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) {
if ((eh = e.hash) == h) {
// 普通节点:hash >= 0
}
else if (eh < 0) // 特殊节点:hash < 0
return (p = e.find(h, key)) != null ? p.val : null;
// 继续链表遍历...
}
return null;
}
putVal()方法中的分支处理
final V putVal(K key, V value, boolean onlyIfAbsent) {
// ...
else if ((fh = f.hash) == MOVED) // hash == -1,扩容中
tab = helpTransfer(tab, f);
else {
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
// 普通链表节点处理
}
else if (f instanceof TreeBin) { // hash == -2,红黑树
// 树节点插入逻辑
}
else if (f instanceof ReservationNode) // hash == -3
throw new IllegalStateException("Recursive update");
}
}
}
}
transfer()扩容方法中的应用
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
// ...
else if ((fh = f.hash) == MOVED) // 已处理的桶
advance = true; // already processed
else {
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
// 普通链表分裂处理
}
else if (f instanceof TreeBin) {
// 红黑树分裂处理
}
}
}
}
}
设计优势
高效的类型判断
- 单一比较:通过
hash < 0
快速识别特殊节点 - 减少instanceof开销:只对少数特殊节点使用instanceof
- 分支预测友好:CPU能更好地预测分支走向
内存布局优化
- 统一存储:所有节点类型都能存储在同一个数组中
- 无需额外标记字段:利用hash字段的符号位作为类型标识
- 缓存友好:减少内存访问次数
并发安全保证
- 原子性判断:hash值读取是原子的
- 一致性视图:通过hash值快速确定节点状态
- 无竞态条件:避免了复杂的类型检查竞态
并发扩容机制
扩容触发与协助
private final void addCount(long x, int check) {
// 更新计数
if ((cs = counterCells) != null ||
!U.compareAndSetLong(this, BASECOUNT, b = baseCount, s = b + x)) {
// 分段计数逻辑
}
// 检查是否需要扩容
if (check >= 0) {
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n) << RESIZE_STAMP_SHIFT;
if (sc < 0) { // 已有线程在扩容
if (/* 扩容即将完成 */) break;
if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt); // 协助扩容
}
else if (U.compareAndSetInt(this, SIZECTL, sc, rs + 2))
transfer(tab, null); // 发起扩容
}
}
}
并发传输算法
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int stride = (NCPU > 1) ? (n >>> 3) / NCPU : n;
if (stride < MIN_TRANSFER_STRIDE) stride = MIN_TRANSFER_STRIDE;
for (int i = 0, bound = 0;;) {
// 认领传输区间
while (advance) {
if (--i >= bound || finishing) advance = false;
else if ((nextIndex = transferIndex) <= 0) {
i = -1; advance = false;
}
else if (U.compareAndSetInt(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ? nextIndex - stride : 0))) {
bound = nextBound; i = nextIndex - 1; advance = false;
}
}
// 处理每个桶
synchronized (f) { // 锁定头节点
if (tabAt(tab, i) == f) {
// 链表分裂:根据 hash & n 分为两组
int runBit = fh & n;
// 优化:找到连续相同bit的尾部
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) { runBit = b; lastRun = p; }
}
// 设置到新表
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd); // 设置转发节点
}
}
}
}
扩容特点:
- 多线程协作:动态认领工作区间
- 最小化锁竞争:只锁定桶的头节点
- 零拷贝优化:连续相同bit的节点直接复用
树化与退化机制
链表转红黑树
private final void treeifyBin(Node<K,V>[] tab, int index) {
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
tryPresize(n << 1); // 优先扩容而非树化
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
synchronized (b) {
if (tabAt(tab, index) == b) {
// 构建双向链表
TreeNode<K,V> hd = null, tl = null;
for (Node<K,V> e = b; e != null; e = e.next) {
TreeNode<K,V> p = new TreeNode<K,V>(e.hash, e.key, e.val, null, null);
if ((p.prev = tl) == null) hd = p;
else tl.next = p;
tl = p;
}
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
TreeBin读写分离
static final class TreeBin<K,V> extends Node<K,V> {
TreeNode<K,V> root;
volatile TreeNode<K,V> first; // 链表头,用于遍历
volatile Thread waiter;
volatile int lockState;
final Node<K,V> find(int h, Object k) {
if (k != null) {
for (Node<K,V> e = first; e != null; ) {
int s; K ek;
if (((s = lockState) & (WAITER|WRITER)) != 0) {
// 有写操作,沿链表查找
if (e.hash == h && ((ek = e.key) == k || k.equals(ek)))
return e;
e = e.next;
}
else if (U.compareAndSetInt(this, LOCKSTATE, s, s + READER)) {
// 获取读锁,使用树查找
try {
p = ((r = root) == null ? null : r.findTreeNode(h, k, null));
} finally {
// 释放读锁
}
return p;
}
}
}
return null;
}
}
计数机制
分段计数策略
@jdk.internal.vm.annotation.Contended
static final class CounterCell {
volatile long value;
}
final long sumCount() {
CounterCell[] cs = counterCells;
long sum = baseCount;
if (cs != null) {
for (CounterCell c : cs)
if (c != null) sum += c.value;
}
return sum;
}
动态扩展计数单元
private final void fullAddCount(long x, boolean wasUncontended) {
int h = ThreadLocalRandom.getProbe();
boolean collide = false;
for (;;) {
CounterCell[] cs; CounterCell c; int n; long v;
if ((cs = counterCells) != null && (n = cs.length) > 0) {
if ((c = cs[(n - 1) & h]) == null) {
// 创建新的计数单元
if (cellsBusy == 0) {
CounterCell r = new CounterCell(x);
if (cellsBusy == 0 && U.compareAndSetInt(this, CELLSBUSY, 0, 1)) {
// 安装新单元
}
}
}
else if (!wasUncontended) // 重新哈希
wasUncontended = true;
else if (U.compareAndSetLong(c, CELLVALUE, v = c.value, v + x))
break; // 成功更新
else if (counterCells != cs || n >= NCPU)
collide = false;
else if (!collide)
collide = true;
else if (cellsBusy == 0 && U.compareAndSetInt(this, CELLSBUSY, 0, 1)) {
// 扩展计数表
counterCells = Arrays.copyOf(cs, n << 1);
}
h = ThreadLocalRandom.advanceProbe(h);
}
}
}
视图实现
KeySetView特殊性
public static final class KeySetView<K,V> extends CollectionView<K,V,K>
implements Set<K>, java.io.Serializable {
private final V value; // 支持添加操作的映射值
public boolean add(K e) {
V v;
if ((v = value) == null)
throw new UnsupportedOperationException();
return map.putVal(e, v, true) == null;
}
}
弱一致性迭代器
static class Traverser<K,V> {
Node<K,V>[] tab;
Node<K,V> next;
TableStack<K,V> stack, spare; // 处理转发节点
final Node<K,V> advance() {
Node<K,V> e;
if ((e = next) != null) e = e.next;
for (;;) {
if (e != null) return next = e;
if ((e = tabAt(t, i)) != null && e.hash < 0) {
if (e instanceof ForwardingNode) {
tab = ((ForwardingNode<K,V>)e).nextTable;
pushState(t, i, n); // 保存状态
continue;
}
else if (e instanceof TreeBin)
e = ((TreeBin<K,V>)e).first;
}
// 状态恢复和索引推进逻辑
}
}
}
设计精髓总结
无锁优化
- 分段锁:只锁桶头节点,最小化锁粒度
- CAS操作:表操作、计数更新大量使用CAS
- 内存屏障:精确控制可见性,避免过度同步
性能优化
- 预期常数时间:链表→红黑树自适应
- 并发扩容:多线程协作,减少停顿时间
- 缓存友好:
@Contended
注解避免伪共享
一致性保证
- 弱一致性:迭代过程中允许并发修改
- happens-before:写操作对后续读操作可见
- 最终一致性:保证操作最终反映到所有视图
这种设计使得 ConcurrentHashMap
在高并发场景下既保证了线程安全,又实现了优异的性能,是Java并发编程的典型范例。
get(Object key)
的实现
get
方法通过分层处理(头节点→特殊节点→链表遍历)实现高效查找,结合无锁设计和原子操作,在保证线程安全的同时最大化性能。特殊节点(如扩容状态)的处理确保并发操作的健壮性,哈希计算和内存访问优化则进一步提升了效率。
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) {
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;
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
计算哈希值 / 定位桶
int h = spread(key.hashCode());
- 作用:计算键的哈希值,并进行扩散处理。
-
spread()
实现:static final int spread(int h) { return (h ^ (h >>> 16)) & HASH_BITS; // HASH_BITS = 0x7fffffff }
h ^ (h >>> 16)
:将哈希码的高16位与低16位异或,增加低位的随机性。& HASH_BITS
:确保结果为正数(最高位为0),因为负哈希值用于特殊节点(如MOVED
)。
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
- 检查表是否初始化:
table
不为空且长度大于0。 - 计算桶索引:
(n - 1) & h
(等价于h % n
)。 - 原子获取头节点:
tabAt()
使用Unsafe
保证可见性:static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) { return (Node<K,V>) U.getReferenceAcquire(tab, ((long)i << ASHIFT) + ABASE); }
ASHIFT
和ABASE
:计算元素在数组中的精确内存偏移。
查找
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
- 条件:头节点的哈希值
eh
等于计算出的哈希值h
。 - 检查键是否相等:通过
==
或equals()
判断。 - 直接返回:匹配则返回头节点的值
e.val
。
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
- 场景:头节点的哈希值为负(特殊节点)。
- 调用
find()
:根据节点类型执行不同逻辑:-
TreeBin
(红黑树根节点,hash=TREEBIN=-2
):final Node<K,V> find(int h, Object k) { // 在红黑树中查找(比较哈希值→键→左右子树) }
-
ForwardingNode
(扩容节点,hash=MOVED=-1
):Node<K,V> find(int h, Object k) { // 到新表 nextTable 中查找 return nextTable != null ? nextTable[(nextTable.length - 1) & h].find(h, k) : null; }
-
ReservationNode
(占位节点,hash=RESERVED=-3
):直接返回null
。
-
遍历链表
while ((e = e.next) != null) {
if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
- 条件:头节点未匹配且非特殊节点(普通链表)。
- 遍历链表:依次检查每个节点的哈希值和键。
- 返回匹配值:找到匹配节点则返回
e.val
。
未找到返回 null
关键设计要点
-
无锁读操作:
- 全程无同步(如
synchronized
),依赖volatile
变量和Unsafe
原子操作保证可见性。 tabAt()
使用getReferenceAcquire
确保读取最新数据。
- 全程无同步(如
-
处理并发扩容:
- 遇到
ForwardingNode
时,通过find()
自动重定向到新表nextTable
。 - 扩容期间读操作无需阻塞。
- 遇到
-
高效处理树化:红黑树(
TreeBin
)的find()
时间复杂度为O(log n)
,且通过读写锁分离保证并发安全。 -
哈希值优化:
spread()
确保哈希值为正数,避免与特殊节点冲突。- 桶索引计算
(n-1)&h
高效且分布均匀。
putVal()
实现
参数验证与初始化
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
- 空值检查:禁止 null 键值(区别于 HashMap)
- 哈希计算:
spread()
处理哈希值(同 get 方法) - binCount:记录桶中节点数(用于树化判断)
主循环(CAS + 同步块)
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh; K fk; V fv;
// 分支处理...
}
自旋保证操作成功,处理五种情况:
分支1: 表未初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
-
initTable()
:while ((tab = table) == null || tab.length == 0) { if ((sc = sizeCtl) < 0) Thread.yield(); // lost initialization race; just spin else if (U.compareAndSetInt(this, SIZECTL, sc, -1)) { // 创建数组,设置 sizeCtl = 0.75 * n } }
- CAS 设置 sizeCtl 为 -1(初始化锁)
- 创建默认容量 16 的数组
- 设置扩容阈值 sizeCtl = 12 (n - n/4)
分支2: 空桶插入(无锁 CAS)
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break;
}
- 原子操作:
casTabAt()
使用 CAS 创建新节点static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) { return U.compareAndSetReference(tab, address, c, v); }
- 成功条件:桶位仍为空(未被其他线程修改)
分支3: 检测到扩容(协助迁移)
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
- MOVED 标识:头节点为 ForwardingNode(hash=-1)
-
helpTransfer()
:- 检查扩容状态(nextTable 非空)
- 调用
transfer()
协助数据迁移 - 完成后返回新表继续操作
分支4: putIfAbsent 快速路径
else if (onlyIfAbsent && fh == hash &&
((fk = f.key) == key || (fk != null && key.equals(fk))) &&
(fv = f.val) != null)
return fv;
- 快速失败:仅当
putIfAbsent=true
时触发 - 无锁检查:不获取锁直接检查头节点
- 条件满足:键匹配且值非空 → 返回现有值
分支5: 哈希冲突处理(同步块)
synchronized (f) {
if (tabAt(tab, i) == f) {
// 链表或树处理...
}
}
5.1 链表操作
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
// 1. 键已存在:更新值(onlyIfAbsent=false)
if (e.hash == hash && ((ek = e.key) == key || ...)) {
oldVal = e.val;
if (!onlyIfAbsent) e.val = value;
break;
}
// 2. 键不存在:链表尾部插入
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<>(hash, key, value);
break;
}
}
}
- 遍历链表:检查键是否存在
- 更新或插入:根据 onlyIfAbsent 决定是否更新
- binCount 计数:记录链表长度
5.2 红黑树操作
else if (f instanceof TreeBin) {
binCount = 2;
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> p = t.putTreeVal(hash, key, value);
if (p != null) { // 键已存在
oldVal = p.val;
if (!onlyIfAbsent) p.val = value;
}
}
-
putTreeVal()
:- 查找键是否存在(O(log n))
- 不存在时插入新节点并平衡红黑树
- binCount=2:树节点统计方式不同
5.3 状态验证
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null) return oldVal;
break;
}
- 树化检查:链表长度 ≥8 时
treeifyBin()
- 实际树化条件:数组长度 ≥64,否则扩容
- 完成条件:找到旧值或成功插入时跳出循环
计数与扩容检查
addCount(1L, binCount);
return null;
-
addCount()
:CounterCell[] cs; long b, s; if ((cs = counterCells) != null || !U.compareAndSetLong(this, BASECOUNT, b = baseCount, s = b + x)) { // 使用 CounterCell 分片计数 } if (check >= 0) { // 检查扩容 while (s >= (long)(sc = sizeCtl) && ...) { transfer(); // 触发扩容 } }
- 计数机制:
- 优先尝试 CAS 更新 baseCount
- 竞争激烈时使用 CounterCell[] 分片计数
- 扩容触发:元素数 ≥ sizeCtl 时启动扩容
关键设计亮点
-
分层锁策略:
- 空桶:无锁 CAS
- 非空桶:synchronized 锁头节点(粒度小)
- 扩容:多线程协作
-
锁降级检测:
synchronized (f) { if (tabAt(tab, i) == f) { // 二次验证
防止加锁过程中桶被修改(如树化/扩容)
-
高效扩容:
- 迁移时旧桶锁住,不影响新桶操作
- ForwardingNode 机制允许读操作无等待
-
计数优化:
- 分片计数 (CounterCell) 避免 CAS 竞争
- 延迟更新:优先更新 baseCount,竞争时转分片
-
树化策略:
- 链表长度 ≥8 且数组长度 ≥64 才树化
- 否则优先扩容(避免小表树化开销)
总结
putVal
方法通过分层锁策略(CAS + synchronized)和状态检测,实现高并发下的安全操作。其核心创新点在于:
- 空桶无锁 CAS 最大化并发
- 非空桶锁分离(每个桶独立)
- 扩容协作机制
- 智能树化策略
配合分片计数和精确的内存操作,在保证线程安全的前提下,性能接近无锁 HashMap。
sizeCtl 机制深度解析
sizeCtl
是 ConcurrentHashMap 中最重要的控制变量,它是一个 volatile int
类型字段,负责协调表初始化、扩容和线程协作。其值在不同阶段有不同含义:
值范围 | 含义 |
---|---|
-1 | 表正在初始化 |
<-1 (负数) |
低16位标识 正在扩容的线程数+1; 高16位:Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1))
|
0 | 默认状态,表示初始容量为 16 |
>0 | 下次扩容的阈值(当前容量 * 负载因子)或初始容量 |
初始化表 (initTable()
):
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0) Thread.yield(); // 其他线程正在初始化
else if (U.compareAndSetInt(this, SIZECTL, sc, -1)) { // CAS 抢锁
// 初始化操作...
sizeCtl = n - (n >>> 2); // 设置阈值 = 0.75*n
}
}
扩容标记机制详解
1. 扩容标记生成
// 在 addCount() 中
int rs = resizeStamp(n) << RESIZE_STAMP_SHIFT;
resizeStamp()
方法生成唯一的扩容标记:
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
- Integer.numberOfLeadingZeros(n):计算 n 的二进制前导零个数
- 例如 n=16 (000...00010000),前导零=27=0x1B
- 位或操作:
| (1 << (RESIZE_STAMP_BITS - 1))
RESIZE_STAMP_BITS = 16
1 << 15 = 0x8000
- 低16位最高位为1(保证之后左移16位变为负数),剩余15位包含前导零信息
示例:n=16 时,sizeCtl = (0x801B << 16) + 2 = 0x801B0000 + 2 = 0x801B0002
扩容设置
// 在 addCount() 中
else if (U.compareAndSetInt(this, SIZECTL, sc, rs + 2))
transfer(tab, null);
- 计算:
+2
:实际表示1 + 初始线程数
- 低16位表示
(扩容线程数 + 1)
- 初始值2 表示有1个线程在扩容(2 = 1 + 1)
- 低16位表示
协助扩容设置
// 在 helpTransfer() 中
if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
}
- sc + 1:增加扩容线程计数
- 低16位增加:从 0x801B0002 → 0x801B0003(表示2个线程)
扩容线程退出机制
// 在 transfer() 中
if (U.compareAndSetInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
// 最后一个线程的收尾工作...
}
步骤解析:
- 减少线程计数:
sc - 1
(低16位减1) - 检查是否最后一个线程:
(sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT
sc - 2
:移除初始线程计数resizeStamp(n) << RESIZE_STAMP_SHIFT
:纯扩容标记(高16位)- 比较:如果不等,说明还有其他线程在工作
状态变化示例:
阶段 | sizeCtl 值 | 二进制表示 | 含义 |
---|---|---|---|
初始 | 0x801B0002 | 10000000 00011011 00000000 00000010 | 1个线程扩容 |
加入 | 0x801B0003 | 10000000 00011011 00000000 00000011 | 2个线程扩容 |
退出 | 0x801B0002 | 10000000 00011011 00000000 00000010 | 1个线程剩余 |
最后 | 0x801B0001 | 10000000 00011011 00000000 00000001 | 比较: 0x801B0001 - 2 = 0x801BFFFF != 0x801B0000 → 非最后线程 |
首次扩容保证机制
如何保证第一个 transfer?只有传入null才会初始化
// 在 addCount() 中
else if (U.compareAndSetInt(this, SIZECTL, sc,
rs + 2))
transfer(tab, null); // 第一个线程执行迁移
- CAS 操作:原子性设置 sizeCtl
- 负值保证:
rs + 2
一定是负数(最高位=1) - 状态隔离:
- 正数:正常状态
- -1:初始化中
- 其他负数:扩容中
扩容状态检测:
// 在 addCount() 中
if (sc < 0) { // 已有扩容进行中
// 检查扩容是否兼容
if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
(nt = nextTable) == null || transferIndex <= 0)
break;
// 加入扩容
if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
当以下任一条件满足时,当前线程不参与扩容:
if (
扩容线程数已达上限(65534) || // sc == rs + MAX_RESIZERS
扩容已结束 || // sc == rs + 1
扩容已完成 || // nextTable == null
无待迁移桶 || // transferIndex <= 0
) {
break; // 不参与扩容
}
sc == rs + MAX_RESIZERS
-
sc
:当前 sizeCtl 值 -
rs
:resizeStamp(n) << RESIZE_STAMP_SHIFT
(扩容标记) -
MAX_RESIZERS
:最大扩容线程数private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1; // RESIZE_STAMP_BITS=16 → MAX_RESIZERS=65535
含义:
检查扩容线程数是否已达上限。当 sc - rs = MAX_RESIZERS
时,表示:
sc = rs + MAX_RESIZERS
= (扩容标记) + 65535
由于 sizeCtl 的低16位表示 (扩容线程数 + 1)
,此条件等价于:
(扩容线程数 + 1) = 65535
即:当前已有 65534 个线程在扩容
sc == rs + 1
-
rs + 1
:(resizeStamp(n) << RESIZE_STAMP_SHIFT) + 1
含义:
检查扩容是否处于结束状态。在扩容过程中:
- 初始状态:
sc = rs + 2
(1个线程) - 线程退出时:
sc = sc - 1
- 当
sc == rs + 1
时:rs + 1 = (rs + 2) - 1 表示所有线程都已退出
(nt = nextTable) == null
含义:
检查扩容是否已完成。nextTable
是扩容中的临时新表:
- 非空:扩容进行中
- null:扩容已完成或未开始
transferIndex <= 0
含义:
检查是否还有待迁移的桶。transferIndex
表示下一个待分配迁移任务的起始索引:
-
0:还有待迁移桶
- ≤0:所有桶已分配完毕
这个条件判断是 ConcurrentHashMap 扩容机制的智能调度中枢,它通过精妙的位运算和状态编码实现了:
- 线程数控制:防止过多线程竞争
- 扩容状态检测:识别开始/结束状态
- 任务分配:根据剩余工作量决策
- 资源管理:避免无效操作
设计原理总结
-
复合状态编码:
- 高16位:扩容标记(表长度指纹)
- 低16位:扩容线程数 + 1
-
无锁协作:
- CAS 更新线程计数
- 避免全局锁竞争
-
状态机转换:这里rs表示resizeStamp(n),更合理
扩容兼容检查:
if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
(nt = nextTable) == null || transferIndex <= 0)
- 确保协助线程扩容的是同一张表
- 防止不同扩容周期混淆
这种精妙的设计使得 ConcurrentHashMap 能在高并发环境下,高效协调多线程完成扩容任务,同时保证数据一致性和操作原子性,是 Java 并发编程的经典范例。
helpTransfer() 方法详解
方法定义与作用
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
// 方法实现...
}
- 核心作用:协助进行中的扩容操作
- 触发时机:当线程执行操作(如 put/remove)时发现桶已被标记为 ForwardingNode
- 设计目标:让多个线程协同完成数据迁移,加速扩容过程
前置条件检查
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
- 三个必要条件:
- 当前表非空 (
tab != null
) - 桶节点是 ForwardingNode 类型
- 新表 nextTable 已创建(扩容进行中)
- 当前表非空 (
ForwardingNode 关键属性:
static final class ForwardingNode<K,V> extends Node<K,V> { final Node<K,V>[] nextTable; // 指向新表的引用 // ... }
扩容状态准备
int rs = resizeStamp(tab.length) << RESIZE_STAMP_SHIFT;
- 计算扩容标记:
resizeStamp(tab.length)
:生成表长度的唯一标识- 左移16位:将标记移到高16位
协作循环
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
// 退出条件检查...
// 尝试加入扩容...
}
- 循环条件:
- 新表未改变 (
nextTab == nextTable
) - 主表未改变 (
table == tab
) - 仍处于扩容状态 (
sizeCtl < 0
)
- 新表未改变 (
退出条件判断
if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
transferIndex <= 0)
break;
- 三种退出情况:
sc == rs + MAX_RESIZERS
:扩容线程数已达上限(65535)sc == rs + 1
:扩容已结束(所有线程退出)transferIndex <= 0
:无待迁移桶
加入扩容
if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
- CAS 操作:增加扩容线程计数 (
sc + 1
) - 执行迁移:调用
transfer()
参与数据迁移 - 退出循环:任务分配成功
何时触发 transfer()
首次触发(扩容发起)
// addCount() 方法中
else if (U.compareAndSetInt(this, SIZECTL, sc,
rs + 2))
transfer(tab, null); // 第一个线程执行迁移
- 触发条件:元素数量超过阈值 (
sizeCtl
) - 执行线程:检测到扩容需求的线程
- 关键动作:
- 创建新数组(2倍容量)
- 设置
transferIndex = 旧表长度
- 初始化迁移状态
协助触发(协作扩容)
// helpTransfer() 方法中
if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab); // 协助线程执行迁移
}
- 触发条件:操作时遇到 ForwardingNode
- 执行线程:任意执行 put/remove 等操作的线程
- 关键动作:
- 增加扩容线程计数
- 参与数据迁移任务
重试触发(迁移过程中)
// transfer() 方法内部
if (i < 0 || i >= n || i + n >= nextn) {
if (finishing) { /*...*/ }
else if (U.compareAndSetInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
// 最后一个线程重新扫描
}
}
- 触发条件:迁移任务完成或需要重新分配
- 执行线程:已完成分配任务的迁移线程
- 关键动作:
- 减少线程计数
- 最后一个线程执行收尾工作
transfer() 方法深度解析
初始化迁移参数
int n = tab.length, stride;
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
- 目的:计算每个线程处理的桶区间(步长)
- 计算逻辑:
- 多核CPU:
(数组长度/8) / CPU核心数
- 单核CPU:处理整个数组
- 多核CPU:
- 最小值限制:
MIN_TRANSFER_STRIDE = 16
,避免任务划分过细 - 设计意图:平衡负载,确保每个线程处理足够多的桶
初始化新数组(仅由首个扩容线程执行)
if (nextTab == null) { // initiating
try {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; // 双倍扩容
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE; // 处理OOME,禁止后续扩容
return;
}
nextTable = nextTab; // 设置全局新表引用
transferIndex = n; // 迁移起始索引(从后向前)
}
- 执行条件:
nextTab == null
(首个扩容线程) - 关键操作:
- 创建双倍大小的新数组
- 设置
transferIndex = n
(从数组末尾开始迁移) - OOME 时设置
sizeCtl=Integer.MAX_VALUE
禁止扩容
准备迁移状态
int nextn = nextTab.length;
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true; // 推进标志
boolean finishing = false; // 完成标志
- ForwardingNode:特殊节点(hash=MOVED),标记已迁移桶
- 状态标志:
advance
:控制是否处理下一个桶finishing
:标记迁移进入收尾阶段
迁移任务分配(核心循环)
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
// 任务分配循环
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing) // 当前区间未完成
advance = false;
else if ((nextIndex = transferIndex) <= 0) { // 所有桶已分配
i = -1;
advance = false;
}
// CAS 分配迁移区间 [nextBound, nextIndex-1]
else if (U.compareAndSetInt(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ? nextIndex - stride : 0))) {
bound = nextBound; // 当前线程负责区间的下界
i = nextIndex - 1; // 从后向前处理
advance = false;
}
}
// 迁移状态检查...
}
- 区间分配:通过
TRANSFERINDEX
的 CAS 操作分配任务区间 - 迁移方向:从数组末尾向前迁移(
i = nextIndex - 1
) - 设计优势:避免头部热点竞争,提高并发效率
迁移完成检测
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) { // 最终完成
nextTable = null; // 清除临时引用
table = nextTab; // 更新主表引用
sizeCtl = (n << 1) - (n >>> 1); // 0.75 * 2n
return;
}
// 当前线程完成工作,减少扩容线程计数
if (U.compareAndSetInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
// 检查是否是最后一个扩容线程
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true; // 进入最终检查
i = n; // 重新扫描整个表
}
}
- 完成条件:
finishing=true
:设置新表,更新阈值(sizeCtl = 1.5n
)- 非最后一个线程:直接退出
- 扩容线程计数:
- 扩容开始时:
sizeCtl = (rs << RESIZE_STAMP_SHIFT) + 2
- 每个线程加入:
sizeCtl + 1
- 每个线程退出:
sizeCtl - 1
- 扩容开始时:
- 最终检查:最后一个线程重新扫描全表
空桶/已迁移 处理
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd); // 标记为已迁移
- 操作:CAS 设置 ForwardingNode
- 目的:标记桶已迁移,其他线程可跳过
已迁移桶处理
else if ((fh = f.hash) == MOVED)
advance = true; // 已处理,直接推进
- 快速跳过:遇到 ForwardingNode 直接处理下一个桶
链表迁移(核心)
synchronized (f) {
if (tabAt(tab, i) == f) { // 双重检查
if (fh >= 0) {
// 1. 计算最后一段连续相同位的节点
int runBit = fh & n;
Node<K,V> lastRun = f;
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
// 2. 创建高低位链表头
if (runBit == 0) {
ln = lastRun;
hn = null;
} else {
hn = lastRun;
ln = null;
}
// 3. 构建高低位链表
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
// 4. 设置新表位置
setTabAt(nextTab, i, ln); // 原位
setTabAt(nextTab, i + n, hn); // 偏移n位
setTabAt(tab, i, fwd); // 设置转发节点
advance = true;
}
// 树节点处理...
}
}
- 优化技巧:
- 最后连续段优化:直接复用最后一段相同位的节点链
- 头插法构建链表:避免尾部遍历,O(1) 插入
- 位运算分桶:
(ph & n) == 0
判断高低位
- 位置计算:
- 低位桶:保持原位
i
- 高位桶:
i + n
(原数组长度)
- 低位桶:保持原位
树节点迁移
else if (f instanceof TreeBin) {
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> lo = null, loTail = null;
TreeNode<K,V> hi = null, hiTail = null;
int lc = 0, hc = 0;
// 1. 遍历树节点
for (Node<K,V> e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode<K,V> p = new TreeNode<K,V>(h, e.key, e.val, null, null);
if ((h & n) == 0) { // 低位
if ((p.prev = loTail) == null)
lo = p;
else
loTail.next = p;
loTail = p;
++lc;
} else { // 高位
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
// 2. 判断是否需要树化
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin<K,V>(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin<K,V>(hi) : t;
// 3. 设置新表
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
- 树迁移特点:
- 拆分为两个链表(低位/高位)
- 根据阈值决定树化或链表化:
UNTREEIFY_THRESHOLD=6
:≤6时转为链表MIN_TREEIFY_CAPACITY=64
:小表不树化
- 重用 TreeBin 对象(当另一子树为空时)
设计亮点总结
-
并行迁移算法:
- 任务划分:
stride
控制任务粒度 - 区间分配:
TRANSFERINDEX
CAS 分配 - 方向优化:从后向前避免竞争
- 任务划分:
-
无锁化设计:
- 空桶:CAS 设置 ForwardingNode
- 已迁移桶:直接跳过
- 冲突桶:synchronized 细粒度锁
-
迁移优化技巧:
- 链表:最后连续段优化 + 头插法
- 树:节点复用 + 动态树化/链表化
- 位运算:高效计算新位置
(ph & n)
-
状态机设计:
advance
:控制处理流程finishing
:标记最终状态sizeCtl
:协调线程退出
-
健壮性保障:
- 双重检查锁:
tabAt(tab, i) == f
- OOME 处理:禁止后续扩容
- 并发控制:CAS + synchronized 协同
- 双重检查锁:
总结
transfer()
方法是 ConcurrentHashMap 高并发的核心,其设计亮点在于:
- 多线程协同迁移的无锁化任务分配
- 精细控制的迁移状态机
- 针对链表和树的不同优化策略
- 全程保证并发访问的可用性
通过 CAS 操作协调多线程,结合细粒度锁和智能的迁移算法,在保证线程安全的同时,实现了高效的并发扩容能力,这也是 ConcurrentHashMap 能支撑高并发场景的关键所在。
computeIfAbsent
首先,我们来看一下这个方法的核心功能和契约:
- 作用:如果指定的
key
不存在或对应的值为null
,则尝试使用给定的mappingFunction
计算其值,并将其存入 Map 中(除非计算结果为null
)。 - 返回值:返回与
key
关联的当前值(已存在的或新计算的)。如果计算结果为null
,则返回null
且不会在 Map 中添加任何映射。 - 核心原子性保证:整个方法的调用是原子性的。对于一个给定的
key
,如果它不存在,mappingFunction
最多只会被调用一次。即使多个线程同时为同一个不存在的key
调用computeIfAbsent
,也只有一个线程会执行mappingFunction
,其他线程会等待计算结果。
这个原子性保证是此方法与 if (!map.containsKey(key)) { map.put(key, value); }
这种“先检查后执行”的非原子操作的根本区别。后者在并发环境下存在竞态条件,可能导致 mappingFunction
被多次调用或数据不一致。
下面我们深入代码,看看 ConcurrentHashMap
是如何实现上述保证的。
// ... existing code ...
public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
if (key == null || mappingFunction == null)
throw new NullPointerException();
int h = spread(key.hashCode());
V val = null;
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) & h)) == null) {
Node<K,V> r = new ReservationNode<K,V>();
synchronized (r) {
if (casTabAt(tab, i, null, r)) {
binCount = 1;
Node<K,V> node = null;
try {
if ((val = mappingFunction.apply(key)) != null)
node = new Node<K,V>(h, key, val);
} finally {
setTabAt(tab, i, node);
}
}
}
if (binCount != 0)
break;
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else if (fh == h // check first node without acquiring lock
&& ((fk = f.key) == key || (fk != null && key.equals(fk)))
&& (fv = f.val) != null)
return fv;
else {
boolean added = false;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == h &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
val = e.val;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
if ((val = mappingFunction.apply(key)) != null) {
if (pred.next != null)
throw new IllegalStateException("Recursive update");
added = true;
pred.next = new Node<K,V>(h, key, val);
}
break;
}
}
}
else if (f instanceof TreeBin) {
// ... (TreeBin logic is similar but for red-black trees) ...
// ... existing code ...
}
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (!added)
return val;
break;
}
}
}
if (val != null)
addCount(1L, binCount);
return val;
}
// ... existing code ...
阶段一:初始化与无限循环
if (key == null || mappingFunction == null)
:首先进行空指针检查,这是Map
接口的基本要求。int h = spread(key.hashCode())
:计算key
的哈希值。spread
函数通过一些位运算让哈希值的高位和低位都参与运算,目的是为了减少哈希冲突,让元素在哈希表中分布得更均匀。for (Node<K,V>[] tab = table;;)
:这是一个无限循环。ConcurrentHashMap
的很多写操作都采用这种模式。如果在循环体内部的某次尝试因为并发冲突(比如 CAS 失败)而失败,循环会继续,直到操作成功为止。
阶段二:处理不同情况
在循环内部,代码根据哈希桶(bin)的当前状态,分情况处理:
-
情况 A:哈希表未初始化
if (tab == null || (n = tab.length) == 0) tab = initTable();
如果内部数组
table
还是null
,当前线程会调用initTable()
来进行线程安全的初始化。 -
情况 B:目标哈希桶为空 (关键的原子性实现)
else if ((f = tabAt(tab, i = (n - 1) & h)) == null) { Node<K,V> r = new ReservationNode<K,V>(); synchronized (r) { if (casTabAt(tab, i, null, r)) { // ... call mappingFunction and set node ... } } // ... }
这是最巧妙的部分之一。如果计算出的哈希桶索引
i
处是空的:- 创建占位符:创建一个临时的
ReservationNode
。这是一个特殊的节点,仅用作占位和锁对象。 - 细粒度锁:
synchronized (r)
对这个临时的占位符节点加锁。这是一个非常细粒度的锁,只影响当前这个哈希桶的初始化操作。 - CAS 占位:在同步块内部,使用
casTabAt
(Compare-And-Swap) 尝试将ReservationNode
放入空桶中。 - 执行计算:如果 CAS 成功,说明当前线程成功“预定”了这个桶。此时,它就可以安全地调用
mappingFunction.apply(key)
来计算值。因为持有锁,并且成功放置了占位符,其他线程无法对这个桶做任何操作。 - 设置结果:
finally
块确保无论计算结果如何(成功、null
或异常),都会用setTabAt
将最终的节点(新节点或null
)设置回哈希桶,替换掉ReservationNode
。
原子性如何保证? 如果两个线程同时发现桶为空,它们都会创建自己的
ReservationNode
。但只有一个线程的casTabAt
会成功。失败的那个线程会重新进入for
循环,再次读取时会发现桶里已经不是null
了(可能是ReservationNode
或其他节点),然后会进入其他逻辑分支。 - 创建占位符:创建一个临时的
对一个刚刚创建的、其他线程都看不见的局部变量 r
加锁,第一眼看上去确实像是多此一举。
然而,这正是实现 computeIfAbsent
原子性的关键所在。这里的 synchronized
并非无用,而是与 CAS
(Compare-And-Swap) 操作结合,实现了一种高效的、细粒度的“占位锁定”机制。
我们来分解一下这个过程:
-
创建局部“锁票”: 当一个线程(比如线程 A)发现某个哈希桶(bucket)为空时,它会创建一个
ReservationNode
实例r
。此时,r
确实是线程 A 的一个局部变量,就像一张它自己打印的“门票”。 -
尝试原子性地“贴上门票”: 关键的一步是
casTabAt(tab, i, null, r)
。这是一个原子操作,它会检查哈希桶i
是否为null
,如果是,就将线程 A 的“门票”r
放入这个桶中。- 由于 CAS 的原子性,对于同一个空桶,只有一个线程能成功完成这个操作。
- 一旦线程 A 成功,这个原本是局部的
r
对象,现在就被发布到了所有线程共享的table
数组中。它从一张“私有门票”变成了一张贴在门上的“已占座”的公开告示。
-
持有锁并执行计算:
synchronized (r)
块在casTabAt
操作之后仍然有效。这意味着成功贴上“门票”的线程 A,在执行mappingFunction
进行耗时计算时,仍然持有r
对象的锁。 -
其他线程的等待:
- 如果另一个线程(线程 B)在线程 A 之后也来访问这个桶,它会发现桶里不再是
null
,而是一个ReservationNode
(也就是线程 A 放入的r
)。 - 线程 B 会进入
else
分支,并尝试执行synchronized (f)
,这里的f
正是线程 A 放入的那个r
对象。 - 由于线程 A 正在计算并且还未释放
r
的锁,线程 B 就会在这里阻塞等待。它不会去执行自己的计算,而是等待线程 A 计算出结果。
- 如果另一个线程(线程 B)在线程 A 之后也来访问这个桶,它会发现桶里不再是
-
完成并唤醒: 线程 A 计算完毕后,会将结果放入桶中(替换掉
ReservationNode
),然后退出synchronized
块,释放锁。此时,等待的线程 B 被唤醒,它可以继续执行并最终拿到线程 A 的计算结果。
这个 synchronized (r)
的目的不是为了锁住一个局部变量,而是为了锁住一个被原子地发布到共享内存中的占位符。它巧妙地将一个局部对象提升为了一个针对单个哈希桶的临时锁,从而保证了在并发场景下,对于同一个不存在的 key,昂贵的计算函数 mappingFunction
只会被执行一次。
-
情况 C:哈希表正在扩容
else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f);
如果桶的头节点哈希值为
MOVED
,说明哈希表正在进行扩容。当前线程不会等待扩容完成,而是会调用helpTransfer
"帮助"扩容,然后在新表上重试操作。这是一种协作式的、无锁的扩容机制。 -
情况 D:键已存在(快速路径)
else if (fh == h && ((fk = f.key) == key || ...)) return fv;
这是一个无锁的快速路径优化。如果桶的第一个节点正好就是我们要找的
key
,并且它的val
不为null
,就直接返回,避免了任何加锁开销。 -
情况 E:哈希桶非空(通用路径)
else { synchronized (f) { if (tabAt(tab, i) == f) { // Double-check // ... traverse list or tree ... } } }
如果桶不为空,且不是以上特殊情况,就进入这个通用处理逻辑:
- 锁住头节点:
synchronized (f)
,这里的f
是桶的头节点。这会锁住整个哈希桶(无论是链表还是红黑树),防止其他线程修改它。 - 双重检查:
if (tabAt(tab, i) == f)
是一种双重检查锁定模式。确保从读到加锁的这段时间里,头节点没有被其他线程改变。 - 遍历查找:在同步块内部,安全地遍历链表或红黑树。
- 如果找到
key
,就直接获取val
并跳出。 - 如果遍历到末尾都没找到,就调用
mappingFunction.apply(key)
计算新值,并创建一个新节点追加到链表或树的末尾。
- 如果找到
原子性如何保证?
mappingFunction
的调用和新节点的插入都发生在synchronized (f)
块内部。因此,对于同一个哈希桶,一次只有一个线程能执行这段代码。如果两个线程为同一个桶里的同一个不存在的key
调用computeIfAbsent
,它们都会尝试获取f
的锁。获得锁的线程会执行计算和插入,释放锁后,另一个线程获得锁,再次遍历时就会发现key
已经存在,于是直接返回值,不会再调用mappingFunction
。 - 锁住头节点:
阶段三:收尾工作
treeifyBin(tab, i)
:如果在一个桶中添加节点后,发现链表长度超过了阈值TREEIFY_THRESHOLD
(默认为8),就会调用此方法将链表转化为红黑树,以提高后续操作的性能。addCount(1L, binCount)
:如果成功添加了新节点,会调用addCount
来增加ConcurrentHashMap
的总大小。这是一个高度优化的计数器,使用分段计数来减少并发争用。
原子性保证总结
computeIfAbsent
的原子性是通过以下机制组合实现的:
- CAS 操作:用于在无锁或低锁的情况下进行状态转换,例如在空桶中放置
ReservationNode
。 - 分段/分桶锁 (Fine-Grained Locking):当必须加锁时,
ConcurrentHashMap
不会锁住整个Map
,而是只锁住需要修改的那个哈希桶的头节点 (synchronized(f)
)。这使得不同桶的操作可以完全并发进行,大大提高了吞吐量。 volatile
读写:tabAt
和setTabAt
等方法内部使用VarHandle
(在旧版本中是Unsafe
),提供了volatile
语义的内存读写,确保一个线程对table
数组的修改对其他线程立即可见。- 协作式扩容:遇到扩容时,线程会主动参与,而不是被动等待,避免了全局暂停。
- 将计算置于锁内:最关键的一点是,可能耗时的
mappingFunction
的调用,被严格地保护在synchronized
块内部。这确保了“计算并放入”这个复合操作的原子性,杜绝了竞态条件。