JJava 知识库
JAVA INTERVIEW

高频面试题

Java集合进阶约 3 分钟

你们用 ConcurrentHashMap 做过本地缓存或并发统计吗?它怎么保证并发安全,使用时还要注意什么?

参考回答约 3 分钟 · 口语表达
先说结论

这个问题我会先说项目结论:JDK 8 的 ConcurrentHashMap 使用数组、链表和红黑树存储数据。读操作主要依靠 volatile 可见性无锁完成;写入空桶时使用 CAS,桶冲突时锁定桶头节点;扩容时多个线程可以协助迁移。

01

我会先交代项目背景和选型结论。对比 JDK 7 与 JDK 8 的结构,理解并发读写、扩容和复合操作。JDK 8 的 ConcurrentHashMap 使用数组、链表和红黑树存储数据。读操作主要依靠 volatile 可见性无锁完成;写入空桶时使用 CAS,桶冲突时锁定桶头节点;扩容时多个线程可以协助迁移。 JDK 7 使用 Segment 分段锁,JDK 8 取消固定分段,降低冲突粒度并提升扩容协作能力。

02

具体落地时,我会沿着实际调用链来讲。沿着「计算哈希定位桶 → 读取 volatile 桶节点 → CAS 初始化或插入 → 桶冲突时局部加锁 → 协助扩容并发布结果」观察输入、状态与输出,这些阶段都可以从日志、指标或源码里验证。 计算哈希定位桶 并发映射仍先通过扰动哈希定位桶,读取路径尽量依靠 volatile 可见性而不获取全局锁。 读取 volatile 桶节点 table 和节点关键字段的发布关系保证已完成写入对后续读可见,但复合业务操作仍可能需要原子 API。 ConcurrentHashMap#putVal:空桶 CAS 与桶首 synchronized。 ConcurrentHashMap#transfer:ForwardingNode 与并行扩容。

03

参数和容量不能靠默认值,我会结合业务量来定。以 1、8、64 线程压测均匀键和单热键;比较 get+put、compute 与 LongAdder,记录吞吐和桶锁等待。

04

效果要用数据证明,线上问题也要能闭环。固定输入和基线 先在没有故障注入的环境执行上述配置,固定数据规模、并发度、运行时版本和预热时间。以「热键占比」为主基线,记录值应满足「应接近业务分布」;同时保存 热点桶锁等待、compute 执行时间,使后续变化能够回到同一时间轴比较。 代码先 get 计数、加一再 put,多线程下两个更新读取同一旧值,ConcurrentHashMap 本身没有损坏但业务复合操作丢失。改用 compute 或 LongAdder 作为值,并确保映射函数短小且无外部阻塞后,更新才真正原子。 get 后 put 的检查再执行竞态:热点桶锁等待:复合更新改为 compute/merge/putIfAbsent。

05

最后我会主动说明这个方案不适合什么场景。方案:更适合的场景:主要收益:代价与边界。 ConcurrentHashMap:高并发通用键值映射:读路径轻、桶级并发:复合操作必须使用原子 API。 同步包装 Map:低并发且需要整体互斥语义:实现简单:全局锁限制吞吐和遍历。 不可变快照:配置等读极多、整批更新:读取无锁且视图一致:更新需复制,实时单键写不合适。 选型至少带上 元素数量、读写比例、遍历方式、并发度和内存预算,并用上面的量化基线验证;