ConcurrentHashMap线程安全靠分段锁(JDK7)或CAS+synchronized单节点锁(JDK8+),读操作无锁,写操作仅锁冲突桶;put流程含CAS插入、树根锁或链表头锁;size()非精确;for-each遍历时修改结构会抛异常;computeIfAbsent嵌套调用同桶key将死锁。

ConcurrentHashMap 的线程安全不是靠 synchronized 锁整个表
它用的是分段锁(JDK 7)或 CAS + synchronized 锁单个 Node(JDK 8+),核心思路是缩小锁粒度。JDK 8 彻底抛弃了 Segment,改用 volatile 修饰的 table 数组 + 对每个桶(bin)头节点加锁。这意味着:只有哈希冲突严重、多个线程同时往同一个桶写时才会真正阻塞。
常见误解是“它比 HashMap 多加了个锁”,其实它根本没锁整个结构——读操作基本无锁(依赖 volatile 和 final 字段的内存语义),写操作只锁必要的一小块。
put 方法在 JDK 8 中如何避免全局竞争
put 执行流程关键点:
- 先通过
spread(key.hashCode())计算索引,再用tabAt(table, i)无锁读取桶首节点 - 如果桶为空,尝试用
casTabAt原子插入;失败则进入自旋重试 - 如果桶是红黑树节点,直接加
synchronized锁住树根节点操作 - 如果是链表,先锁住该链表头节点(
synchronized (f)),再遍历插入或更新 - 当链表长度 ≥ 8 且
table.length ≥ 64时,才触发树化;否则先扩容
注意:size() 不是 O(1),它要累加所有 CounterCell 并加上 baseCount,可能有轻微误差;如需精确计数,应配合 LongAdder 单独维护。
立即学习“Java免费学习笔记(深入)”;
为什么不能用 for-each 遍历 ConcurrentHashMap 修改结构
虽然 ConcurrentHashMap 的迭代器是弱一致性的(weakly consistent),但以下操作仍会抛 ConcurrentModificationException:
- 在
for (String s : map.keySet()) { map.remove(s); }中边遍历边删 - 调用
clear()或replaceAll()期间,另一个线程正在put
正确做法是使用 computeIfAbsent、merge、replace 等原子方法,或显式用 keySet().forEach(...) 配合不可变操作。若必须边遍历边删,请用 mappingCount() 判断后,改用 remove(key, value) 带条件删除。
computeIfAbsent 的陷阱:lambda 内部不能递归调用自己
computeIfAbsent 在 key 不存在时执行 mappingFunction,并将结果写入 map —— 但这个过程是加锁的(锁对应桶)。如果 lambda 里又调用了 computeIfAbsent 且目标 key 落在同一桶,会死锁。
示例错误写法:
map.computeIfAbsent("a", k -> map.computeIfAbsent("b", k2 -> "value"));
更隐蔽的问题是:lambda 中触发了可能映射到同一桶的其他 key 计算(比如字符串哈希巧合相同)。规避方式包括:
- 确保 mappingFunction 是纯函数、无副作用、不访问本 map
- 用
compute替代,手动控制锁范围 - 对高风险场景,提前预估 key 分布,用不同哈希策略隔离桶
实际开发中,最容易被忽略的是 computeIfAbsent 的锁范围和嵌套调用风险——它不像表面看起来那么“安全”。










