concurrenthashmap线程安全靠cas+synchronized细粒度锁与volatile可见性保障,读操作无锁,写操作锁单桶,但复合逻辑(如get+put)非原子,size()弱一致,遍历为弱一致性。

ConcurrentHashMap 的线程安全性不是靠“一把大锁”实现的,而是通过精细控制原子性与可见性,在读多写少场景下兼顾性能与正确性。分析它在多线程下的表现,关键不在“它是否安全”,而在于“它在哪种操作上保证了什么,又在哪种边界下可能失效”。
原子性:分段控制 + CAS + 锁粒度下沉
ConcurrentHashMap 并不保证所有操作都原子——它的原子性是按操作类型和数据结构层级设计的:
- put、remove、replace 等写操作:对单个桶(bin)加锁(JDK8 中是 synchronized 锁住链表头节点或红黑树根节点),配合 CAS 更新 table 数组引用和 sizeCtl 等控制变量,确保单个 key-value 的插入/删除/替换是原子的。
- compute、merge 等复合操作:内部会先获取当前值,再计算新值,最后 CAS 写回;若期间被其他线程修改,会重试,本质上是乐观锁机制,也具备原子性语义。
- get 操作完全无锁:不加任何同步,也不触发 volatile 写,但依赖 Node.val 和 next 字段声明为 volatile ——这保障的是读取过程中的可见性,而非 get 本身的原子性(它本来就是单次读)。
- size() 不是强原子的:它返回的是一个近似值,因为需要累加多个 counterCell 的值,而计数更新本身用的是 LongAdder 的分段 CAS,无法严格保证调用瞬间的全局精确值。
可见性:volatile 字段 + happens-before 链路
ConcurrentHashMap 对可见性的保障是隐式嵌入在结构设计里的,不靠 synchronized 块来建立内存屏障,而是依赖 volatile 语义和 JMM 规则:
- Node.val 和 Node.next 声明为 volatile:这意味着每次 put 成功写入一个新节点后,该节点的 key、val、next 等字段对其他线程立即可见;后续线程通过 get 遍历链表时,能按正确顺序看到已发布的节点结构。
- table 数组引用本身是 volatile 的:扩容时 new table 被赋值给 table 字段前,所有已迁移节点的数据已写入新数组;volatile 写保证其他线程一旦看到新 table 引用,就一定能看见其内部已初始化完成的节点内容(happens-before 传递性)。
- ForwardingNode 的存在强化可见性边界:当某个桶正在迁移,原位置会置为 ForwardingNode。其他线程遇到它,会主动去新表查找——这个跳转动作本身就依赖于 ForwardingNode 的可见性,而它也是 volatile 写入的。
不能假设原子性 & 可见性的典型误区
开发者容易高估 ConcurrentHashMap 的“全能性”,以下情况它不提供额外保障:
- 复合逻辑非原子:比如“先 get 再 put”的判断式操作(if (map.get(k) == null) map.put(k, v))不是原子的,中间可能被其他线程插入,必须用 putIfAbsent 或 computeIfAbsent 替代。
- 遍历过程不快照隔离:entrySet().iterator() 返回的迭代器是弱一致性(weakly consistent),可反映某时刻的部分状态,但不阻塞写操作,也不保证遍历期间不漏项或重复;它不提供类似数据库的可重复读语义。
- 自定义对象字段仍需自行保证可见性:如果 value 是一个普通 Java 对象(如 User),其内部字段(name、age)未用 volatile 或同步保护,那么即使 ConcurrentHashMap 保证了 User 实例引用的可见性,也不能保证该实例内部字段的最新值被其他线程看到。
验证思路:用代码看清行为边界
想确认某行为是否受保障?别只查文档,动手写最小复现:
- 用两个线程交替执行 put 和 get,观察是否出现 null 或旧值 → 验证可见性是否及时;
- 100 个线程并发调用 computeIfAbsent,检查最终 size 是否等于线程数 → 验证复合操作原子性;
- 在遍历 entrySet 同时持续 put 新元素,打印迭代器结果并比对实际 size → 理解弱一致性表现。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











