ConcurrentHashMap 是 Java 并发包里最常用的线程安全映射实现,它的高性能并不来自一把大锁,而是把 CAS 指令和 synchronized 块按场景拆分使用。JDK 8 之后,它放弃了分段锁,改用数组加链表或红黑树的结构,对每个桶头节点做细粒度控制。当多个线程同时操作不同桶时几乎互不干扰,只有在同一个桶发生写冲突时才对该桶加锁。

一、CAS 与 synchronized 在 ConcurrentHashMap 中的分工
在 ConcurrentHashMap 里,CAS 全称是 Compare And Swap,它是一种CPU级原子指令,可以在无锁情况下判断某个内存值是否等于预期值,如果是则更新,否则不做操作并返回失败。源码中大量使用 Unsafe 类的 compareAndSwapObject 等方法完成表初始化、桶头替换、计数累加。
synchronized 块则用于真正发生哈希冲突的写场景。例如两个线程都要向同一个桶插入节点,CAS 抢到桶头的人直接写入,失败的一方会对该桶的头节点对象加锁,再在链表或树中查找并追加。这种组合让无竞争路径完全无锁,有竞争路径只锁一个桶,而不是整个表。
1.1 为什么不全用 CAS
理论上所有操作都能用 CAS 循环重试实现,但链表遍历、红黑树旋转等过程状态复杂,单纯 CAS 会导致代码难以维护且重试开销大。只对头节点和关键字段使用 CAS,把复杂结构修改交给 synchronized,是工程上最平衡的做法。
另外,synchronized 在 JVM 后续版本已经做了锁升级优化,从偏向锁到轻量级锁再到重量级锁,低竞争时代价很小。因此即使偶尔进入同步块,也不会像早期版本那样严重拖慢性能。
二、核心源码机制拆解
以 put 方法为例,当表还未初始化时,使用 CAS 控制 sizeCtl 字段完成并发初始化;表已存在时,先定位桶下标,若桶为空则用 CAS 放入头节点。若桶不为空,则进入 synchronized 块操作链表或树。
// 简化逻辑示意,非完整源码
if (tab == null || (n = tab.length) == 0) {
// 使用 CAS 初始化表
if (casTabAt(tab, 0, null, new Node<K,V>(0, null, null, null))) {
// 初始化成功
}
}
int i = (n - 1) & hash;
Node<K,V> f = tabAt(tab, i);
if (f == null) {
// 桶空,CAS 放入
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
return;
} else {
synchronized (f) {
// 对桶头加锁,遍历或插入
if (tabAt(tab, i) == f) {
// 链表或树操作
}
}
}
2.1 sizeCtl 的 CAS 用法
sizeCtl 是一个控制变量,负数代表表正在初始化或扩容,正数代表阈值。多个线程调用 put 时,只有一个能通过 CAS 把 sizeCtl 设为负值抢到初始化权,其余线程自旋等待,这避免了重复建表。
类似的,计数桶 CounterCell 也用 CAS 累加,减少竞争。只有当 CAS 累加失败多次才可能触发扩容,而扩容时依然用 CAS 分配迁移任务,用 synchronized 锁住单个桶做数据转移。
三、实战:自定义无锁化计数器组件
我们可以借鉴 ConcurrentHashMap 的思路,写一个分片计数器:把计数拆成多个 Cell,更新时先 CAS 改本地 Cell,冲突时再用 synchronized 合并,从而降低锁粒度。
public class ShardCounter {
private final AtomicLong[] cells;
private final int mask;
public ShardCounter(int power) {
int size = 1 << power;
cells = new AtomicLong[size];
for (int i = 0; i < size; i++) cells[i] = new AtomicLong(0);
mask = size - 1;
}
public void add(long x, int threadId) {
int idx = threadId & mask;
AtomicLong cell = cells[idx];
// 先 CAS 尝试无锁累加
long old = cell.get();
if (!cell.compareAndSet(old, old + x)) {
// CAS 失败说明有竞争,对单个 cell 加锁再改
synchronized (cell) {
cell.set(cell.get() + x);
}
}
}
public long sum() {
long s = 0;
for (AtomicLong c : cells) s += c.get();
return s;
}
}
3.1 代码解析与优缺点
上面的 ShardCounter 把不同 threadId 映射到不同 cell,大多数时候各线程改各自的 AtomicLong,依靠 CAS 完成无锁写入。只有哈希到同一 cell 且 CAS 冲突时才进入 synchronized,锁范围仅为一个对象。
优点显而易见:高并发下冲突概率随分片数增加而下降,吞吐明显优于全局 synchronized 长整型。缺点是 sum 不是精确瞬间值,且内存占用随分片翻倍。这和 ConcurrentHashMap 的 size 方法类似,属于弱一致统计。
四、常见误区与性能注意点
有人以为 synchronized 在 ConcurrentHashMap 里代表“退化成锁表”,其实它只锁当前桶头节点,其他桶完全可并发。也有开发者频繁调用 size 想拿精确值,导致反复累加 CounterCell,反而拖累性能。
4.1 伪共享问题
当多个 Cell 或桶节点在内存中紧挨着,CPU 缓存行会同时加载它们,一个核修改会让其他核缓存失效。JDK 用 @Contended 或手动填充降低伪共享,实战中若自己写分片结构,也建议在对象间加长整型填充。
总结来说,掌握 ConcurrentHashMap 的 CAS 与 synchronized 协作,核心是把“无锁快路径”和“加锁慢路径”分离,按数据粒度而非整体加锁。把它用在本地缓存、指标统计、任务分片提交等场景,就能写出既安全又高效的并发代码。
ConcurrentHashMapCASsynchronized修改时间:2026-08-01 07:36:29