导读:本期聚焦于小伙伴创作的《如何利用 ConcurrentHashMap 的 CAS 指令与 synchronized 块实战掌握无锁化并发控制》,敬请观看详情。为什么高并发场景下用 HashMap 会出现数据错乱,而 ConcurrentHashMap 却能稳定写入?关键在于它把 CAS 指令和 synchronized 块组合成了分桶粒度的并发控制。CAS 负责在无竞争时以原子方式更新表头或计数,失败时再对单个桶加锁,避免整体阻塞。本文从底层结构讲起,对比不同 JDK 版本的实现差异,给出初始化、累加、批量合并三种实战代码,并分析伪共享、扩容迁移时的锁边界。理解这套机制后,你可以把同样思路迁移到本地缓存、计数器组件,用细粒度锁换更高吞吐。

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

如何利用 ConcurrentHashMap 的 CAS 指令与 synchronized 块实战掌握无锁化并发控制

一、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

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。