《Java ConcurrentHashMap 源码:线程安全的底层逻辑》
·
Java ConcurrentHashMap 源码分析:线程安全的底层逻辑
ConcurrentHashMap 是 Java 并发包(java.util.concurrent)中的核心类,用于实现线程安全的哈希表。它通过高效的并发控制机制,避免全局锁的开销,从而在高并发场景下提供高性能。以下我将逐步解析其线程安全的底层逻辑,基于 Java 8 及之后版本的源码实现(Java 7 及之前使用分段锁,但 Java 8 优化为更精细的锁机制)。源码分析基于 OpenJDK,确保真实可靠。
1. 线程安全的核心机制:CAS 和 synchronized
ConcurrentHashMap 的线程安全依赖于两个关键底层技术:
- CAS (Compare-And-Swap):一种无锁算法,用于原子更新变量(如桶头节点)。CAS 操作在硬件层面支持,高效且避免阻塞。
- synchronized 关键字:用于对单个桶(bin)加锁,粒度更细,减少锁竞争。
在 Java 8 源码中,put 方法的核心逻辑体现了这一点:
- 当插入元素时,首先计算哈希值(使用
spread方法扩散哈希,减少冲突)。 - 如果桶为空,使用 CAS 原子设置桶头节点(避免锁)。
- 如果桶非空,使用
synchronized对桶加锁,然后处理链表或红黑树(树化优化)。
数学上,哈希扩散函数可表示为:$h = (h \oplus (h \gg 16)) & \text{HASH_BITS}$,其中 $h$ 是原始哈希值,$\gg$ 表示右移,$&$ 是按位与操作,确保哈希值在有效范围内。
源码片段(简化版):
final V putVal(K key, V value, boolean onlyIfAbsent) {
// 计算哈希
int hash = spread(key.hashCode());
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable(); // 初始化表
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// CAS 设置桶头节点(无锁)
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break;
} else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f); // 处理扩容
else {
V oldVal = null;
synchronized (f) { // 对桶加锁
if (tabAt(tab, i) == f) {
if (fh >= 0) {
// 处理链表插入
// ... 省略链表遍历和更新代码
} else if (f instanceof TreeBin) {
// 处理红黑树插入
// ... 省略树操作代码
}
}
}
if (oldVal != null) return oldVal;
}
}
return null;
}
2. 底层数据结构:数组 + 链表/红黑树
ConcurrentHashMap 使用动态数组(table)存储桶,每个桶可以是链表或红黑树(当链表长度超过阈值时树化,提升查询效率)。线程安全通过以下方式保证:
- 桶级锁:
synchronized只锁定当前桶,其他桶可并发访问。 - 原子操作:使用
sun.misc.Unsafe类(如tabAt和casTabAt)实现内存可见性和原子更新。
扩容机制(transfer 方法)也线程安全:
- 多线程协作扩容:一个线程触发扩容时,其他线程可帮助迁移数据。
- 使用
ForwardingNode标记迁移中的桶,避免重复操作。
数学上,扩容策略涉及负载因子:当元素数量超过 $capacity \times loadFactor$ 时触发扩容,新容量为旧容量的 2 倍。例如,旧容量 $n$,新容量 $2n$。
3. 高效并发的原因
- 低锁粒度:相比
Hashtable的全局锁,ConcurrentHashMap 锁粒度更细,减少竞争。 - 无锁读:
get方法完全无锁,通过 volatile 变量保证内存可见性。 - 统计优化:
size方法使用CounterCell数组分散计数,避免单点竞争。
源码中 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 || (key != null && key.equals(ek))))
return e.val;
}
}
return null;
}
4. 局限与最佳实践
- 局限:在极端高并发下,CAS 失败可能导致自旋开销;扩容时短暂阻塞。
- 最佳实践:设置合理初始容量和负载因子(默认 0.75),避免频繁扩容。推荐使用 Java 8+ 版本,性能更优。
通过以上分析,ConcurrentHashMap 的线程安全源于精细的锁设计和原子操作,源码实现高效且可靠。建议结合 JDK 源码(如 ConcurrentHashMap.java)深入学习,提升并发编程技能。
更多推荐


所有评论(0)