concurrenthashmap 采用 cas 与 synchronized 协作机制:cas 用于空桶插入、状态更新等无竞争场景,synchronized 仅锁冲突桶头节点,读操作彻底无锁依赖 volatile;cas 有节制重试并规避 aba,多变量更新由分段计数器处理。
concurrenthashmap 的 cas 与 synchronized 并非互斥手段,而是分工明确的协作机制:cas 处理无竞争的原子写入,synchronized 应对真实竞争时的临界区保护。掌握它,关键不是背代码,而是理解“什么情况走无锁、什么情况必须加锁”。
CAS 主要用于空桶插入和状态变量更新
CAS 在 ConcurrentHashMap 中最典型的用途是向空桶写入第一个节点,以及控制表初始化、扩容等全局状态。这些操作天然具备“一次成功、失败即退”的特征,适合乐观策略。
- 插入新键值对时,先用 tabAt() 读取桶头节点;若为 null,立即调用 casTabAt() 尝试原子设置——成功则完成插入,全程无锁
- 初始化哈希表时,通过 CAS 修改 sizeCtl 判断是否由当前线程执行 initTable(),避免多个线程重复初始化
- 扩容触发阶段,也是靠 CAS 更新 sizeCtl 为负值来“抢到”扩容权,失败者转为协助迁移
synchronized 只锁真正冲突的桶头节点
当 CAS 失败(比如桶已非空),说明存在并发写入,此时才启用 synchronized 锁定该桶的头节点(Node 对象本身),而非整个 Map 或 Segment。锁的范围极小,且只在必要时出现。
- 锁对象是具体桶的首节点(synchronized(f)),不同桶之间完全不干扰
- 进入同步块后,会再次检查 tabAt(table, i) == f,防止被其他线程抢先迁移或修改,保证操作前提依然成立
- 链表插入、红黑树转换、树节点插入等结构变更,都在这个同步块内完成,逻辑集中、边界清晰
读操作彻底无锁,靠 volatile 保障可见性
get() 方法从不加锁,也不用 CAS,仅依赖 Node 内部字段(如 val、next)声明为 volatile,配合 CPU 内存屏障,确保读线程总能看到最新写入结果。
- 即使另一线程正在同一桶中执行 synchronized 插入,读线程也能安全读取已存在的节点
- 扩容过程中,旧表节点被迁移到新表,但迁移是逐桶进行的;未迁移的桶仍可正常读写,已迁移的桶会在新表上继续服务
- 这种设计让读性能几乎不受并发写影响,特别适合读多写少场景
实战中要注意 CAS 的局限性和应对方式
CAS 不是万能的,它在高冲突、多字段协同更新等场景下会失效。ConcurrentHashMap 的设计已规避多数陷阱,但开发者需知其边界。
- ABA 问题被规避:sizeCtl 等关键状态变量虽用 CAS,但其语义设计天然防 ABA(例如 -1 表示初始化中,-(1+N) 表示 N 个线程扩容中,数值含义唯一)
- 自旋重试有节制:空桶插入失败后不会无限重试,而是快速退到 synchronized 路径,避免 CPU 空转
- 不支持多变量原子更新:例如不能用单个 CAS 同时更新 size 和 table,所以 size() 使用分段计数器 CounterCell + volatile long baseCount,再辅以 CAS 更新计数器数组











