Java中如何通过LinkedHashMap的访问顺序实现基础LRU缓存淘汰机制?

2026-04-29 08:572阅读0评论SEO问题
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计842个文字,预计阅读时间需要4分钟。

Java中如何通过LinkedHashMap的访问顺序实现基础LRU缓存淘汰机制?

由于+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」。

  • 建议禁止存 null value(可在 put() 前校验并抛 NullPointerException
  • 若必须支持,改用 Optional<V> 包装返回值,或额外维护一个 Set<K> 记录已存在的 key
  • computeIfAbsent() 等衍生方法在 accessOrder 模式下同样触发重排序,但要注意 lambda 内部不要递归调用自身缓存

accessOrder 模式本质是链表维护策略,不解决原子性、可见性、null 语义这些上层问题 —— 它只是让“最久未用”这个判定变得廉价可得。

标签:Java

本文共计842个文字,预计阅读时间需要4分钟。

Java中如何通过LinkedHashMap的访问顺序实现基础LRU缓存淘汰机制?

由于+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」。

  • 建议禁止存 null value(可在 put() 前校验并抛 NullPointerException
  • 若必须支持,改用 Optional<V> 包装返回值,或额外维护一个 Set<K> 记录已存在的 key
  • computeIfAbsent() 等衍生方法在 accessOrder 模式下同样触发重排序,但要注意 lambda 内部不要递归调用自身缓存

accessOrder 模式本质是链表维护策略,不解决原子性、可见性、null 语义这些上层问题 —— 它只是让“最久未用”这个判定变得廉价可得。

标签:Java