一级缓存用 HashMap 而非 ConcurrentHashMap,因其线程封闭、生命周期短,无需并发安全;应确保 key 稳定、value 无强引用大对象,并用 computeIfAbsent 安全写入,避免副作用与重复构建。

为什么不用 ConcurrentHashMap 而用 HashMap 做一级缓存?
因为一级缓存(如方法内临时缓存、请求级缓存)生命周期短、线程封闭,根本不需要并发安全。用 ConcurrentHashMap 反而带来不必要的 CAS 开销和内存屏障成本。只有在多线程共享、且写入频繁的场景下才需要它;本地一级缓存通常由单一线程创建、使用、丢弃,HashMap 足够快,也更轻量。
- 缓存对象必须是线程局部的(比如放在
ThreadLocal里,或作为方法参数传入) - 不要把它塞进静态字段或 Spring 的
@Component单例里,否则会引发并发问题 - 如果缓存需跨方法复用但仍在同一线程内,用
ThreadLocal是常见做法
computeIfAbsent 是最安全的写入入口,但别滥用
这个方法能避免重复计算 + 防止重复 put,适合「查不到就构建再存」的典型缓存逻辑。但它不是万能的:如果构建过程抛异常,HashMap 不会存任何东西,但调用方可能没意识到构建失败了。
- 构建逻辑必须幂等,不能有副作用(比如发 HTTP 请求、改 DB)
- 如果构建耗时较长,又没加锁,多个线程同时触发(在非线程封闭场景下)会导致重复构建——所以务必确认是否真“线程封闭”
- 它内部仍会调用
hash()和equals(),key 类型要确保这两个方法稳定可靠(比如自定义类必须重写hashCode()和equals())
示例:
Map<string user> cache = new HashMap(); User user = cache.computeIfAbsent(userId, id -> loadFromDB(id)); </string>
容量控制和 GC 友好性比“过期”更重要
一级缓存不追求 TTL 或自动淘汰,而是靠作用域自然回收。强行加定时清理或 LRU 逻辑,反而增加复杂度和 CPU 占用。
- 不要给
HashMap设超大初始容量(比如new HashMap(10000)),除非你明确知道这次缓存一定装得下上万条 - 更推荐用
new HashMap(expectedSize)预估大小,减少 rehash 次数 - key 和 value 尽量避免强引用大对象(如整个
byte[]或未关闭的流),否则 GC 压力会上升 - 如果缓存中 value 是可变对象,后续修改会影响缓存一致性——要么存不可变副本,要么注明“只读使用”
容易被忽略的坑:key 的生命周期比 map 还短
常见错误是把局部变量、lambda 表达式引用、或带栈帧的对象当 key,结果 key 提前被 GC,导致缓存失效或查不到。
- key 必须是稳定标识符,比如字符串 ID、整数主键、不可变 DTO(且已正确实现
hashCode/equals) - 避免用
new Object()、匿名内部类实例、或含this引用的 lambda 当 key - 如果 key 是临时构造的 POJO,检查它的字段是否全为 final,是否所有字段都参与了
hashCode计算
缓存性能好不好,不取决于用了多少高级技巧,而在于它是不是真的“用完即扔”,以及 key 是否足够干净、稳定。一旦出现“有时命中有时不命中的问题”,八成是 key 的生命周期或相等性逻辑出了问题。










