ConcurrentHashMap线程安全但非所有操作原子:JDK7用分段锁(Segment),JDK8改用CAS+synchronized锁单桶,size()通过baseCount+CounterCell分散计数;复合操作、弱一致性迭代、跨key事务需额外同步。

ConcurrentHashMap 是 Java 并发编程的高频考点,大厂面试不只考“它线程安全”,更关注你是否真正理解它的设计哲学、演进逻辑和实战边界。答对“分段锁”或“CAS + synchronized”只是入场券,真正拉开差距的是——你能说清 为什么这么设计、在哪种场景下会失效、和 HashMap / Collections.synchronizedMap 本质区别在哪。
底层结构:从 JDK7 到 JDK8 的范式转移
JDK7 用 Segment 数组 + HashEntry 链表,本质是“锁分段”——把整个哈希表切分成 16 段(默认),每段独立加锁。看似并发度高,但存在明显缺陷:跨段操作(如 size())需锁全部 segment;扩容成本高;内存占用大。
JDK8 彻底重构:取消 Segment,改用 Node 数组 + 链表/红黑树,核心靠 CAS + synchronized 锁单个链表头节点。这意味着:
- 锁粒度最小化到“桶级别”,不是“段”,也不是“整个 Map”
- putVal() 中只有 hash 冲突且链表首节点被占用时,才对该桶头节点加 synchronized
- TreeBin(红黑树根节点)用特殊的读写锁机制,保证并发读不阻塞、写操作串行化
- size() 不再依赖全局锁,而是通过 baseCount + CounterCell[] 分散计数,用 CAS 累加
线程安全 ≠ 所有操作都原子 —— 常见认知陷阱
很多人误以为“ConcurrentHashMap 是线程安全的 Map,所以所有方法都可无脑并发调用”。这是危险误区:
立即学习“Java免费学习笔记(深入)”;
- 复合操作非原子:比如“先 get 再 put”(如缓存穿透防护中的 ifAbsent)、“遍历中删除”等,仍需手动加锁或使用 computeIfAbsent/computeIfPresent 等原子方法
- 迭代器弱一致性:entrySet().iterator() 不抛 ConcurrentModificationException,但不保证反映最新修改——它基于创建快照,可能漏掉后续 put/remove,也可能看到重复元素(尤其扩容时)
- 不能替代显式同步逻辑:比如银行转账涉及两个账户余额更新,ConcurrentHashMap 单个 map 无法保证跨 key 操作的事务性,必须引入外部锁或分布式锁
高频面试追问与硬核回答要点
面试官常借小问题考察你的源码级理解:
- “为什么 JDK8 不用 ReentrantLock 而用 synchronized?” → 因为 synchronized 在 JDK6+ 已优化(偏向锁、轻量级锁、锁消除),且锁对象是具体 Node,生命周期短、竞争低,synchronized 更轻量;ReentrantLock 功能过剩,还带来额外内存开销
- “扩容时怎么保证读写不乱?” → 扩容由多个线程协作完成,每个线程负责迁移若干桶;旧数组未删,新数组逐步填充;读操作先查新表,查不到再查旧表;写操作若发现桶是 ForwardingNode(标记扩容中),则协助迁移或重试
- “get() 方法为什么不用加锁?” → 因为 Node 的 val 和 next 字段都用 volatile 修饰,保证可见性;且 get 过程无状态修改,纯读操作天然安全
实战选型:什么情况下不该用 ConcurrentHashMap?
它不是银弹。以下场景建议换方案:
- 读少写多,且写操作常需条件判断(如“值大于 100 才更新”)→ 优先考虑 StampedLock 或 LongAdder + 显式锁
- 需要强一致遍历(如导出全量数据做校验)→ 改用 Collections.synchronizedMap(new HashMap()) + 外部 synchronized(iterate)
- Key/Value 类型复杂,且频繁执行 computeXXX 方法导致锁竞争上升 → 拆分粒度,按业务维度建多个小 ConcurrentHashMap,降低单桶冲突概率
- 要求严格顺序(如 FIFO 缓存淘汰)→ 它不保序,应选 ConcurrentLinkedQueue + 外部 Map 索引 或 LinkedBlockingDeque
不复杂但容易忽略。真正通关的关键,是从“我会用”走向“我懂它为何这样活”。翻一翻 JDK8 的 ConcurrentHashMap.java 第 1200 行左右的 transfer() 和 2900 行的 addCount(),比背十道八股文更有底气。


















