因为linkedhashmap不支持自动淘汰、过期时间和线程安全,需重写removeeldestentry并结合容量与过期双重判断,且底层不能换concurrenthashmap,get时须用system.nanotime()惰性校验过期并原子移除。

为什么不用 LinkedHashMap 直接继承就完事?
因为默认的 LinkedHashMap 只支持访问顺序(accessOrder = true)或插入顺序,但不自动触发淘汰——你得自己在 put 之后判断 size 并手动 removeEldestEntry。更关键的是:它不支持过期时间,也没有线程安全机制。
常见错误现象:ConcurrentModificationException 在多线程读写时频繁抛出;缓存项“永远不淘汰”,哪怕设置了 TTL;removeEldestEntry 返回 true 后,被删的 entry 没有回调通知,无法清理关联资源。
- 必须重写
removeEldestEntry,且返回逻辑要结合容量 + 过期双重判断 - 如果用
java.util.concurrent.ConcurrentHashMap替代底层存储,会丢失访问顺序——LinkedHashMap的双向链表特性不能被并发哈希表替代 - 过期检查不能只靠写入时戳,得在
get时惰性校验,否则长期不访问的脏数据会滞留
get 时怎么安全检查过期并剔除?
不能只比对 System.currentTimeMillis() 和写入时间戳,因为系统时钟可能回拨(NTP 同步、虚拟机休眠),导致大量误淘汰。要用 System.nanoTime() 记录相对过期时间,再配合一个基准偏移量做转换。
使用场景:缓存项带 expireAfterWrite = 60_000L(毫秒),但应用部署在时钟不稳的容器中。
立即学习“Java免费学习笔记(深入)”;
-
get方法里先取节点,再用System.nanoTime() - node.accessNanos > node.expireNanos判断是否过期 - 过期后必须调用
remove并返回null,不能只返回旧值 - 注意:
remove操作需同步链表和哈希表两处结构,否则出现“查不到但链表里还连着”的脏状态
淘汰策略和容量控制怎么不拖慢 put?
LRU 的核心开销在链表节点移动(moveToHead)和尾部驱逐(removeTail)。如果每次 put 都检查容量并遍历链表找最老项,O(n) 复杂度直接废掉性能。
性能影响:10 万条缓存下,未优化的 put 平均耗时从 50ns 涨到 8μs 以上。
- 把容量上限检查放在
afterNodeInsertion回调里,由LinkedHashMap自动触发,避免手动扫描 - 驱逐动作必须原子:先从链表断开节点,再从哈希表
remove,顺序反了会导致内存泄漏 - 不要在驱逐时执行用户自定义的
onEvict回调——它可能阻塞,应异步投递到线程池
为什么 WeakReference 或 SoftReference 不适合做 LRU 缓存主体?
GC 触发时机不可控,JVM 可能在内存充足时就回收 SoftReference,也可能在 OOM 前都不动它。而 LRU 是确定性淘汰策略,依赖精确的访问序和容量阈值。
兼容性影响:OpenJDK 17+ 对 SoftReference 的保留策略已变更,默认更激进回收;GraalVM Native Image 中弱引用行为与 HotSpot 完全不同。
-
WeakReference一 GC 就丢,根本撑不到 LRU 淘汰逻辑执行 - 若用弱引用来包装 value,需额外维护强引用防止提前回收——这又回到原始问题:谁来管这个强引用的生命周期?
- 真正该用弱引用的地方只有 key(比如缓存 classloader 敏感对象),但 key 弱化后,
get时需重建 key 哈希,成本更高
最易被忽略的一点:缓存项的 hashCode 和 equals 必须稳定。如果 value 是可变对象,后续 get 时因字段变化导致哈希错位,就会查不到——这不是淘汰问题,是设计缺陷。










