LinkedHashMap 无法原生支持 TTL,需封装带时间戳的 Entry 并在 get/put 中按需清理过期项,兼顾 LRU 顺序与时间维度;推荐用 Caffeine 或 Guava Cache 等成熟方案。

直接用 LinkedHashMap 无法原生支持“过期自动剔除”(TTL),它只管访问顺序和容量淘汰。要实现带过期语义的 LRU,必须在 LinkedHashMap 的基础上叠加时间维度控制——核心思路是:**保留 LRU 的访问序能力,同时为每个 entry 记录写入/最后访问时间,并在 get/put 时主动检查并清理过期项**。
关键改造点:把时间信息嵌入缓存条目
不能只存 value,要封装成带时间戳的对象。例如定义一个内部类:
-
Entry
{ V value; long expireAt; } —— expireAt 可设为绝对时间(如 System.nanoTime() + ttlNanos) - put 时生成该对象并存入 LinkedHashMap;get 时先判断 expireAt 是否已过,过期则 remove 并返回 null
- 注意:LinkedHashMap 的
removeEldestEntry仅用于容量淘汰,不负责过期判断,过期必须在 get/put 中显式触发
避免被动等待:每次访问都做一次轻量级清理
LRU 缓存本身不轮询,所以过期清理只能“按需驱动”。推荐在 get 和 put 方法开头统一加一段扫描逻辑:
- 遍历 LinkedHashMap 头部若干项(比如前 3 个),检查是否过期;一旦发现过期,立即 remove
- 不遍历全量——否则 O(n) 破坏 LRU 的 O(1) 保证;只扫头部是因为最老项最可能过期,且符合 LRU 淘汰倾向
- 也可配合 size() 判断:若缓存 size 明显大于活跃数据量,再触发一轮更广扫描(如前 10%)
线程安全与时间精度取舍
多线程环境下需注意两个细节:
- System.nanoTime() 比 System.currentTimeMillis() 更适合 TTL,避免系统时钟回拨导致误判过期
- 若用 Collections.synchronizedMap 包装 LinkedHashMap,get/put 需同步块包裹;但更推荐用
ReentrantLock控制临界区,尤其清理逻辑需独占访问链表头 - 不要在 removeEldestEntry 里做耗时操作(如 I/O 或复杂计算),它在 put 内部被调用,会拖慢主流程
简化方案:用现成工具类替代手写
如果项目允许引入依赖,比手写更稳:
-
Caffeine:提供
Caffeine.newBuilder().maximumSize(1000).expireAfterWrite(10, TimeUnit.MINUTES),自动融合 LRU + TTL,且基于 Window TinyLfu 提升命中率 -
Guava Cache:支持
expireAfterWrite和expireAfterAccess,底层也是访问序+定时/惰性清理结合 - 纯 JDK 场景下,可封装一个
TimedLRUCache类,复用 LinkedHashMap 的 accessOrder=true 和 removeEldestEntry,再叠加上述时间封装与按需清理逻辑










