LinkedHashMap 的 accessOrder=true 支持 LRU,因其在 get/put 时将节点移至链表尾,头节点即最久未使用项;需显式构造并重写 removeEldestEntry() 控制容量,该方法仅在 put 后触发,不用于 get。

为什么 LinkedHashMap 的 accessOrder = true 能支持 LRU
因为 LinkedHashMap 在启用访问顺序模式后,每次 get() 或 put() 已存在 key 时,都会把对应节点移到链表尾部;而链表头部自然就成了「最久未使用」的入口。这省去了手写双向链表 + 哈希映射的复杂度,是 JDK 内置的轻量级 LRU 底层支撑。
注意:必须显式调用带三个参数的构造函数,仅靠继承或默认构造不会开启该特性。
-
new LinkedHashMap(initialCapacity, loadFactor, true)—— 第三个参数才是关键 - 如果只传两个参数(如
new LinkedHashMap(16, 0.75f)),accessOrder默认为false,此时仍是插入顺序,无法用于 LRU - 重写
removeEldestEntry()是触发淘汰的唯一可控入口,不重写就永远不会删数据
如何用 removeEldestEntry() 实现容量上限控制
这个方法在每次 put() 后被调用,传入的是当前即将成为「最老」的条目(即链表头节点)。返回 true 就删掉它,false 则保留。它是 LRU 容量策略的开关,不是回调钩子,也不在 get() 时触发。
private static final int MAX_SIZE = 3;
Map<String, String> lruCache = new LinkedHashMap<>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
return size() > MAX_SIZE;
}
};
- 判断条件写
size() > MAX_SIZE,而不是>=——否则插入第 4 个元素时会删掉第 1 个,最终最多存 3 个,符合直觉 - 不要在里面做耗时操作(如日志、IO),它在
put()的同步路径上,影响性能 - 该方法接收的
eldest是「当前链表头」,但此时新 entry 还没加入,所以size()是旧尺寸;等方法返回true后,才真正移除它并插入新 entry
get() 触发重排序但不触发淘汰,这点容易误判
很多人以为访问一个已有 key 会检查是否超限并可能淘汰,其实不会。removeEldestEntry() 只在 put() 和 putAll() 后调用,get() 只负责把命中项移到链表尾,更新访问序,不改变 size(),也绝不会触发删除逻辑。
- 这意味着:纯读场景下缓存可无限增长(只要不
put新 key)——但这不是 bug,是设计使然 - 如果你需要「读老化」(比如希望长期不写的 key 也被淘汰),就得自己封装一层,在
get()里手动检查并清理,或者改用ConcurrentHashMap + Timestamp自研 -
getOrDefault()、computeIfAbsent()等方法的行为要特别小心:computeIfAbsent()内部会先get()再put(),所以可能触发淘汰;而getOrDefault()不会
线程安全与实际使用中的边界情况
LinkedHashMap 本身不是线程安全的,即使只读,多线程 get() 也可能因内部结构变动(如 resize)导致 ConcurrentModificationException。生产环境不能裸用。
- 简单方案:用
Collections.synchronizedMap(new LinkedHashMap(...)),但会锁整个 map,吞吐低 - 更优解:用
java.util.concurrent.ConcurrentHashMap配合外部时间戳 + 定期清扫,或直接选用caffeine这类成熟库 - 注意
key的equals()/hashCode()必须稳定——如果 key 是可变对象且修改了影响 hash 的字段,后续get()就会失效,还可能破坏链表结构 - 序列化时,
accessOrder模式下的迭代顺序会被保留,但反序列化后若未重新访问,链表顺序仍按原 dump 时状态,这点测试时容易忽略
真正难的从来不是「怎么让 LinkedHashMap 支持 LRU」,而是想清楚:你的淘汰是否必须严格按访问时间?是否接受写阻塞?key 是否绝对不可变?这些决定了你到底该不该从这里起步。

















