核心思想:分段锁与CAS
ConcurrentHashMap 的设计目标是在保证线程安全的同时,尽可能提高并发访问的效率。它的核心思想是减小锁的粒度,从 Hashtable 的全局锁进化到分段锁(JDK 7)再到 CAS + synchronized(JDK 8及以后)。
我们将以目前主流的 JDK 8+ 的实现为重点进行讲解。
第一部分:ConcurrentHashMap (JDK 8+) 源码详解
JDK 8 对 ConcurrentHashMap 进行了翻天覆地的重写,摒弃了分段锁,采用了与 HashMap 更相似的 Node数组+链表+红黑树结构,但通过大量使用 CAS操作 和 对单个桶(链表头/树根)施加 synchronized 锁 来实现并发控制。
1. 关键属性
// 核心数组,懒初始化,大小总是2的幂次方。
transient volatile Node<K,V>[] table;
// 在调整大小时使用的下一个表,非空时表示正在扩容。
private transient volatile Node<K,V>[] nextTable;
// 基础计数器,主要在没有竞争时使用,通过CAS更新。
private transient volatile long baseCount;
// 表初始化和扩容的控制标识符。
// 为负数:表正在初始化或扩容。-1表示初始化,-(1 + 活跃的扩容线程数)
// 为 0:创建时的初始值。
// 为正数:下一次扩容的阈值。
private transient volatile int sizeCtl;
sizeCtl 是一个极其重要的控制变量,它用单个变量通过不同的数值状态巧妙地控制了多种并发场景(初始化、扩容)。
2. put(K key, V value) 流程
put 方法是理解其并发设计的核心。它的流程可以概括为:CAS失败则重试,找到桶后就锁住这个桶进行操作。
final V putVal(K key, V value, boolean onlyIfAbsent) {
// 1. 参数校验,key和value不能为null (CHM不允许null键值)
if (key == null || value == null) throw new NullPointerException();
// 2. 计算key的散列值(通过spread方法,混合高低位,减少碰撞)
int hash = spread(key.hashCode());
int binCount = 0; // 记录链表长度,用于判断是否要树化
// 3. 自旋(死循环),直到操作成功(插入、更新)才退出
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 情况A:表还未初始化,则初始化表
if (tab == null || (n = tab.length) == 0)
tab = initTable(); // 使用CAS竞争初始化权
// 情况B:计算出的桶(i)位置为空
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 使用CAS操作,尝试将新Node放入这个空桶
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break; // CAS成功,插入完成,跳出循环
}
// 情况C:检测到该桶的节点的hash为MOVED,表示表正在扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f); // 当前线程协助扩容
// 情况D:桶不为空,且不在扩容状态,则锁定这个桶(头节点f)
else {
V oldVal = null;
synchronized (f) { // 对桶的头节点加锁,粒度非常细
if (tabAt(tab, i) == f) { // 再次验证头节点没被改变(双检锁思想)
if (fh >= 0) { // 普通链表节点
binCount = 1;
// 遍历链表...
for (Node<K,V> e = f;; ++binCount) {
// 找到key,则更新value
// 没找到,则在链表尾部添加新Node
}
}
else if (f instanceof TreeBin) { // 红黑树节点
// 在红黑树中执行插入操作...
}
}
}
// 锁释放后,判断是否需要将链表转为红黑树
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// 4. 更新元素计数,并判断是否需要扩容
addCount(1L, binCount);
return null;
}
核心要点:
- 无锁化尝试:遇到空桶时,使用 CAS 无锁操作进行插入,这是最高效的方式。
- 细粒度锁:遇到哈希冲突时,只对单个桶(链表或树的头节点)进行
synchronized加锁。这意味着可以同时有16个(默认容量)甚至更多的线程同时进行put操作,只要它们操作的桶不同。 - 协助扩容 (Helping Transfer):如果线程发现当前表正在扩容 (
fh == MOVED),它不会阻塞等待,而是主动参与帮助数据迁移,加快扩容过程。这是一种“化敌为友”的巧妙设计。 - CAS控制初始化与计数:表的初始化和元素总数的更新 (
addCount) 都通过CAS操作完成,避免了全局锁。
3. 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) { // volatile读获取头节点
if ((eh = e.hash) == h) { // 如果头节点就是要找的
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// 如果hash为负,说明是特殊节点(正在扩容或是树),调用对应的find方法
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;
}
核心要点:
- 无锁:整个过程没有使用任何锁,仅通过
volatile和UNSAFE提供的原子性volatile读(tabAt) 来保证每次读到的是最新写入的值。 - 弱一致性:由于无锁,
get操作可能无法完全反映最近某个put操作的结果(比如正在遍历的链表下一刻被另一个线程修改了),这是为了性能而做的妥协,符合ConcurrentHashMap的弱一致性迭代器语义。但它能保证最终能读到某个已完成操作的结果。
第二部分:ConcurrentHashMap vs. HashMap
| 特性 | HashMap (非线程安全) | ConcurrentHashMap (线程安全) |
|---|---|---|
| 线程安全 | 否。多线程下使用会导致数据错乱,如死循环(JDK7)、数据丢失等。 | 是。采用CAS + synchronized 实现高效并发。 |
| Null 键/值 | 允许。可以有一个null键和多个null值。 | 不允许。put(null, ...) 会直接抛出 NullPointerException。 |
| 迭代器 | Fail-Fast。在迭代过程中如果结构被修改(除了Iterator.remove),会立即抛出 ConcurrentModificationException。 | Weakly Consistent (弱一致性)。迭代器反映创建时或之后的某个状态,可以安全地在迭代时进行并发修改,不会抛异常。 |
| 性能 | 单线程下最高。无任何同步开销。 | 高。并发环境下性能远高于 Hashtable 和 Collections.synchronizedMap。读操作完全无锁,写操作锁粒度很细。 |
| 底层实现 (JDK8+) | 数组 + 链表 + 红黑树 | 数组 + 链表 + 红黑树 |
| 锁的机制 | 无锁 | CAS + synchronized。锁的粒度是单个桶(链表头节点/树根)。 |
| 扩容 | 单线程操作,执行期间所有操作阻塞。 | 多线程协同扩容。当前线程插入时发现正在扩容,会帮助迁移数据。 |
| 继承体系 | 继承自 AbstractMap | 继承自 AbstractMap,但实现了 ConcurrentMap 接口。 |
总结
HashMap是高效的非线程安全散列表,适用于单线程环境或作为局部变量。ConcurrentHashMap是HashMap的线程安全版本,但其实现并非简单粗暴地加锁。它通过 CAS无锁算法 和 细粒度的同步锁(synchronized),将锁的范围从整个表缩小到单个桶,极大地提升了并发性能。其协助扩容机制更是精妙,将并发操作的瓶颈转化为优势。
简单来说,ConcurrentHashMap 的设计是 “读操作完全无锁,写操作冲突时才最小化加锁” 这一并发设计哲学的经典体现。

被折叠的 条评论
为什么被折叠?



