只有对象构造成本高、无状态/可重入、实例可控且重复率高时才考虑缓存;优先用ConcurrentHashMap,避免ThreadLocal伪缓存和WeakHashMap/SoftReference等错误方案。

缓存对象前先确认是否真的需要
Java 中多数场景下 new 对象开销极小,JVM 的 TLAB(Thread Local Allocation Buffer)和逃逸分析已大幅优化短生命周期对象分配。盲目缓存反而引入线程安全、内存泄漏、状态污染等风险。只有满足以下条件时才考虑缓存:
- 对象构造成本高(如含复杂初始化逻辑、IO、反射调用)
- 对象是无状态或明确可重入(如
SimpleDateFormat本身不可缓存,但包装为线程安全的DateTimeFormatter后可复用) - 实例数量可控且重复率高(如固定配置类、枚举包装器)
用 ConcurrentHashMap 实现简单键值缓存
避免手写双重检查锁或误用 static final 单例——那是单例模式,不是缓存。真正按 key 复用对象时,ConcurrentHashMap 是最直接的选择。注意几个关键点:
- key 必须正确实现
equals()和hashCode(),推荐用不可变类型(如String、Integer或自定义record) - 不建议用
computeIfAbsent包裹耗时操作,否则并发下可能多次执行初始化逻辑(JDK 8 的该方法不保证只执行一次) - 若初始化逻辑较重,应配合
AtomicReference或Future防止重复构建
private static final ConcurrentHashMapCACHE = new ConcurrentHashMap<>(); public HeavyObject getHeavyObject(String key) { return CACHE.computeIfAbsent(key, k -> { // ⚠️ 注意:此处仍可能被多个线程同时调用 return new HeavyObject(k); }); }
慎用 ThreadLocal 做“伪缓存”
ThreadLocal 不是缓存机制,而是线程隔离的变量副本。常见误用是把它当对象池用,比如缓存 StringBuilder 或 JSONWriter。问题在于:
- 线程复用(如 Tomcat 线程池)会导致
ThreadLocal持有对象长期不释放,引发内存泄漏 - 未调用
remove()时,对象会随线程存活而滞留,尤其在使用static ThreadLocal时更危险 - 无法控制总实例数,容易掩盖真实资源瓶颈
如果真要复用,优先选择显式对象池(如 commons-pool2),或直接使用 JDK 自带的无状态工具类(如 DateTimeFormatter 是线程安全的,无需缓存)。
立即学习“Java免费学习笔记(深入)”;
避免引用已废弃的缓存方案
不要用 WeakHashMap 存储业务对象做“自动清理缓存”,它只对 key 弱引用,value 仍强引用,GC 不会因此回收 value;也不要用 SoftReference 实现 LRU 缓存——JVM 的软引用回收策略与堆压力相关,行为不可控,易导致缓存击穿。真正需要容量限制和淘汰策略时,应使用成熟库:
- Guava Cache:支持
maximumSize、expireAfterWrite、refreshAfterWrite - Caffeine:性能更好,API 兼容 Guava,推荐新项目首选
- Spring Cache 抽象:适合已有 Spring 环境,但底层仍需指定具体实现(如 Caffeine)
缓存最难的从来不是“怎么放进去”,而是“什么时候清掉”和“多线程下怎么不出错”。这两点没想清楚前,new 一个新对象,往往是最稳妥的选择。










