要用 LinkedHashMap 实现真正可用的轻量级 LRU 缓存,关键不是继承后随便重写一个方法,而是把三个动作做准:开启访问顺序、精准控制淘汰逻辑、避开高频变量场景下的典型陷阱。必须显式启用 accessOrder = true,使用三参数构造器 new LinkedHashMap<K,V>(initialCapacity, 0.75f, true),正确重写 removeEldestEntry 返回 size() > capacity,并规避 value 可变性、null 值歧义、key 哈希失效及并发安全问题。

要用 LinkedHashMap 实现真正可用的轻量级 LRU 缓存,关键不是继承后随便重写一个方法,而是把三个动作做准:开启访问顺序、精准控制淘汰逻辑、避开高频变量场景下的典型陷阱。
必须显式启用 accessOrder = true
LinkedHashMap 默认是插入顺序(accessOrder = false),此时 get() 不会移动节点位置,整个 LRU 行为完全失效——看着像 LRU,实际是 FIFO。必须用三参数构造器:new LinkedHashMap<K,V>(initialCapacity, 0.75f, true)
第三个参数 true 才激活访问顺序。
- loadFactor 建议保持 0.75f:调高易触发哈希表扩容,打乱链表稳定性;调低则浪费空间
- accessOrder 是 final 字段,构造后不可修改,不能“先 new 再设”
- Java 8+ 中
get()和put()都会触发顺序更新,但getOrDefault()、computeIfAbsent()等不会——它们绕过访问钩子
正确重写 removeEldestEntry 控制容量
这个方法只在每次 put() 或 putAll() 后被调用一次,用于决定是否删除链表头部(即最久未用)的条目。它不响应 get(),也不保证线程安全,逻辑必须轻量、确定。
- 标准写法:
return size() > capacity;(capacity是你期望的最大条目数) - 避免
size() >= capacity:会导致刚达上限就删,可能误删刚插入项 - 避免
size() > capacity + 1:会导致超容不删,缓存持续膨胀 - 方法体内禁止 I/O、日志、锁、复杂计算——它同步执行在
put调用栈中
避开高频变量场景的典型问题
高频变量生命周期短、访问密集,也更容易暴露底层细节漏洞。
-
value 可变性风险:若 value 是
ArrayList或自定义对象,get()返回的是原始引用,外部修改会污染缓存。建议返回Collections.unmodifiableList或按需深拷贝 -
null value 歧义:
put(key, null)合法且参与排序,但get()返回null无法区分「未命中」和「值为 null」。如业务允许 null,统一用Optional<V>包装 -
key 哈希失效:若用自定义对象作 key,必须正确重写
equals()和hashCode(),且字段不可变;否则哈希错位、重复插入、淘汰错位
并发安全不能靠“包装”来凑
LinkedHashMap 本身非线程安全。多个线程同时 get/put 可能导致 ConcurrentModificationException 或链表断裂。
- 低并发可加
ReentrantLock包裹关键操作,比Collections.synchronizedMap()更可靠——后者不同步removeEldestEntry() - 不推荐用于高并发或带过期时间的复杂场景;这类需求建议直接用 Caffeine 或 Guava Cache
- 它适合单线程或低并发下读多写少的热点数据,比如用户会话 ID、模板参数、限流令牌等


















