ConcurrentHashMap 通过细粒度加锁、无锁读和原子方法实现高性能线程安全;应避免复合操作,改用 putIfAbsent、computeIfAbsent 等原子方法;get 无锁且高效,无需额外同步;初始化容量需按预估键数向上取整至2的幂次并预留余量。

ConcurrentHashMap 是 Java 中应对百万级高并发读写的首选线程安全 Map,它不靠全局锁,而是通过细粒度加锁 + 无锁读 + 原子方法组合实现高性能与安全性兼顾。关键不在“能不能用”,而在于“怎么用才不出错、不拖慢”。
避免复合操作,只用原子方法
像 if (!map.containsKey(k)) map.put(k, v) 这类判断+写入的两步操作,即使用了 ConcurrentHashMap,依然存在竞态:两个线程同时判断 key 不存在,接着都执行 put,导致后写覆盖前写。必须改用内置原子方法:
-
putIfAbsent(k, v):key 不存在才插入,一次 CAS 完成,绝对安全 -
computeIfAbsent(k, f):key 不存在时调用函数计算值并写入,函数体只被一个线程执行(适合查库加载缓存) -
merge(k, v, (old, new) -> old + new):安全累加计数器,无需 synchronized -
replace(k, oldValue, newValue):带条件更新,防止误覆盖
读多写少场景下,get 就够用,别加锁
ConcurrentHashMap 的 get() 是无锁操作,依赖 volatile 字段保证可见性,响应极快。在百万 QPS 的缓存读取中,直接 map.get(key) 即可,性能接近 HashMap。不要因为“怕不安全”就给 get 加 synchronized 或 ReentrantLock——这反而会把并发读变成串行,吞吐暴跌。
初始化容量与扩容策略要预估
默认初始容量是 16,负载因子 0.75,但百万级并发下频繁扩容会触发全表 rehash,造成短暂卡顿甚至 OOM。建议按预估总键数向上取整到 2 的幂次,并预留余量:
立即学习“Java免费学习笔记(深入)”;
- 预计存 200 万个 key → 初始容量设为
new ConcurrentHashMap(2 (即 2097152) - 避免运行时扩容:扩容由多个线程协作完成,虽不阻塞读,但写线程需参与迁移,增加开销
- 不推荐调大负载因子(如设为 0.9),会加剧哈希冲突,链表变长,影响 put/get 性能
遍历与统计要接受弱一致性
ConcurrentHashMap 的迭代器是弱一致的:不抛 ConcurrentModificationException,但可能漏掉刚插入的项,或看到已删除项的旧值。这对监控、日志等非强一致性场景完全可接受;但若业务要求精确遍历(如导出全量数据),应考虑:
- 用
mappingCount()替代size()(后者需遍历统计,结果可能不准) - 避免在循环中调用
remove()或put()—— 虽然安全,但可能让后续元素跳过 - 真需要强一致快照?可先
new ArrayList(map.entrySet())复制一份再遍历(注意内存开销)
不复杂但容易忽略:真正压测到百万级时,瓶颈往往不在 ConcurrentHashMap 本身,而在你是否误用了非原子逻辑、是否盲目同步、是否让 compute 函数做了耗时 IO。把原子方法用对,容量设合理,读写各司其职,它就能稳扛住。



















