Java中如何通过LinkedHashMap的访问顺序实现基础LRU缓存淘汰机制?
- 内容介绍
- 文章标签
- 相关推荐
本文共计842个文字,预计阅读时间需要4分钟。
由于+LinkedHashMap+在构造时传入+true+作为第三个参数,会将内部双向链表的更新逻辑从插入顺序切换为访问顺序:
注意:仅 get() 触发重排序;put() 中若 key 已存在,也会触发访问顺序更新;但 put() 新增 key 时只是追加到尾部,不改变其他节点相对位置。
如何重写 removeEldestEntry() 控制缓存容量
LinkedHashMap 提供了钩子方法 removeEldestEntry(),它在每次 put() 后被调用,返回 true 就自动删除最老的条目(即链表头)。这是实现容量上限的关键,不是靠外部轮询或定时清理。
- 必须继承
LinkedHashMap并重写该方法,不能直接实例化后修改 - 判断逻辑通常只看
size() > capacity,不要在里面做耗时操作(如日志、IO) - 该方法在
put()内部同步执行,无需额外加锁,但整个缓存仍需考虑并发安全
class LRUCache<K, V> extends LinkedHashMap<K, V> { private final int capacity; LRUCache(int capacity) { // accessOrder = true super(capacity, 0.75f, true); this.capacity = capacity; } @Override protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { return size() > capacity; } }
并发场景下直接用 LinkedHashMap 子类会出什么问题
LinkedHashMap 本身不是线程安全的,多线程同时 get() 和 put() 可能导致链表结构错乱、死循环(JDK 7/8 中曾有典型 bug),甚至 ConcurrentModificationException。
立即学习“Java免费学习笔记(深入)”;
- 简单加
synchronized方法会导致吞吐量骤降,尤其读多写少场景 - 用
Collections.synchronizedMap()包装后,虽然方法级同步,但removeEldestEntry()不在同步块内,可能失效 - 更稳妥的做法是用
ReentrantLock手动控制get()/put()的临界区,或者改用ConcurrentHashMap+ 自定义队列(但那就脱离LinkedHashMap方案了)
别忽略 get() 返回 null 时的边界行为
当缓存中不存在 key,get() 返回 null,此时不会触发链表调整 —— 这符合预期。但容易被忽略的是:put(key, null) 是合法操作,且会正常参与 LRU 排序;如果业务允许 value 为 null,那 get() 返回 null 就无法区分「未命中」和「命中但值为 null」。
- 建议禁止存
nullvalue(可在put()前校验并抛NullPointerException) - 若必须支持,改用
Optional<V>包装返回值,或额外维护一个Set<K>记录已存在的 key -
computeIfAbsent()等衍生方法在 accessOrder 模式下同样触发重排序,但要注意 lambda 内部不要递归调用自身缓存
accessOrder 模式本质是链表维护策略,不解决原子性、可见性、null 语义这些上层问题 —— 它只是让“最久未用”这个判定变得廉价可得。
本文共计842个文字,预计阅读时间需要4分钟。
由于+LinkedHashMap+在构造时传入+true+作为第三个参数,会将内部双向链表的更新逻辑从插入顺序切换为访问顺序:
注意:仅 get() 触发重排序;put() 中若 key 已存在,也会触发访问顺序更新;但 put() 新增 key 时只是追加到尾部,不改变其他节点相对位置。
如何重写 removeEldestEntry() 控制缓存容量
LinkedHashMap 提供了钩子方法 removeEldestEntry(),它在每次 put() 后被调用,返回 true 就自动删除最老的条目(即链表头)。这是实现容量上限的关键,不是靠外部轮询或定时清理。
- 必须继承
LinkedHashMap并重写该方法,不能直接实例化后修改 - 判断逻辑通常只看
size() > capacity,不要在里面做耗时操作(如日志、IO) - 该方法在
put()内部同步执行,无需额外加锁,但整个缓存仍需考虑并发安全
class LRUCache<K, V> extends LinkedHashMap<K, V> { private final int capacity; LRUCache(int capacity) { // accessOrder = true super(capacity, 0.75f, true); this.capacity = capacity; } @Override protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { return size() > capacity; } }
并发场景下直接用 LinkedHashMap 子类会出什么问题
LinkedHashMap 本身不是线程安全的,多线程同时 get() 和 put() 可能导致链表结构错乱、死循环(JDK 7/8 中曾有典型 bug),甚至 ConcurrentModificationException。
立即学习“Java免费学习笔记(深入)”;
- 简单加
synchronized方法会导致吞吐量骤降,尤其读多写少场景 - 用
Collections.synchronizedMap()包装后,虽然方法级同步,但removeEldestEntry()不在同步块内,可能失效 - 更稳妥的做法是用
ReentrantLock手动控制get()/put()的临界区,或者改用ConcurrentHashMap+ 自定义队列(但那就脱离LinkedHashMap方案了)
别忽略 get() 返回 null 时的边界行为
当缓存中不存在 key,get() 返回 null,此时不会触发链表调整 —— 这符合预期。但容易被忽略的是:put(key, null) 是合法操作,且会正常参与 LRU 排序;如果业务允许 value 为 null,那 get() 返回 null 就无法区分「未命中」和「命中但值为 null」。
- 建议禁止存
nullvalue(可在put()前校验并抛NullPointerException) - 若必须支持,改用
Optional<V>包装返回值,或额外维护一个Set<K>记录已存在的 key -
computeIfAbsent()等衍生方法在 accessOrder 模式下同样触发重排序,但要注意 lambda 内部不要递归调用自身缓存
accessOrder 模式本质是链表维护策略,不解决原子性、可见性、null 语义这些上层问题 —— 它只是让“最久未用”这个判定变得廉价可得。

