LRU算法本地实现

方法一:基于LinkedHashMap实现LRU

使用 LinkedHashMap 实现 LRU(最近最少使用)缓存非常简单,因为 LinkedHashMap 本身提供了按访问顺序排序和移除最老条目的钩子方法。

实现原理

  1. 构造 LinkedHashMap 时设置 accessOrder=true

    这样每次调用 getput 时,被访问的条目会被移动到链表的末尾,链表头部就是最久未使用的条目。

  2. 重写 removeEldestEntry 方法

    Map 大小超过设定的最大容量时,返回 trueLinkedHashMap 就会自动移除头部的(最老的)条目。

代码示例

java 复制代码
import java.util.LinkedHashMap;
import java.util.Map;

public class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private final int maxCapacity;

    /**
     * 构造 LRU 缓存
     * @param maxCapacity 最大容量
     */
    public LRUCache(int maxCapacity) {
        // 初始容量:默认16,负载因子0.75,accessOrder=true 表示按访问顺序排序
        super(16, 0.75f, true);
        this.maxCapacity = maxCapacity;
    }

    /**
     * 当 Map 大小超过最大容量时,移除最老的条目
     * @param eldest 最老的条目
     * @return 是否移除该条目
     */
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > maxCapacity;
    }

    // 测试代码
    public static void main(String[] args) {
        LRUCache<Integer, String> cache = new LRUCache<>(3);

        cache.put(1, "A");
        cache.put(2, "B");
        cache.put(3, "C");
        System.out.println(cache); // {1=A, 2=B, 3=C}

        // 访问 1,它会被移动到末尾
        cache.get(1);
        System.out.println(cache); // {2=B, 3=C, 1=A}

        // 插入新条目,此时容量超限,移除最老的 2
        cache.put(4, "D");
        System.out.println(cache); // {3=C, 1=A, 4=D}
    }
}

关键点说明

  • super(16, 0.75f, true)

    第三个参数 true 表示启用"访问顺序",而非默认的插入顺序。

  • removeEldestEntry

    每次 put 之后会自动调用该方法,判断是否需要移除头部最老的条目。这里直接比较当前大小与最大容量。

  • 线程不安全

    上述实现不是线程安全的,如果需要在多线程环境下使用,可以包装为 Collections.synchronizedMap,或者使用 ConcurrentLinkedHashMap(第三方库)。

扩展:泛型与线程安全版本

如果需要线程安全,可以这样包装:

java 复制代码
LRUCache<Integer, String> cache = new LRUCache<>(3);
Map<Integer, String> synchronizedCache = Collections.synchronizedMap(cache);

但注意 removeEldestEntry 的调用会在同步块内部自动处理。

方法二:基于 Guava Cache 和 Caffeine

1. 添加依赖

Maven

xml 复制代码
<!-- Guava -->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>33.2.1-jre</version>
</dependency>

<!-- Caffeine -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>3.1.8</version>
</dependency>

Gradle

groovy 复制代码
implementation 'com.google.guava:guava:33.2.1-jre'
implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8'

2. Guava Cache 示例(基于 maximumSize 的 LRU)

java 复制代码
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;

import java.util.concurrent.TimeUnit;

public class GuavaLRUExample {
    public static void main(String[] args) {
        // 创建最大容量为 3 的缓存,基于访问顺序(LRU)
        Cache<String, String> cache = CacheBuilder.newBuilder()
                .maximumSize(3)                 // 最多 3 个条目
                .expireAfterAccess(10, TimeUnit.MINUTES) // 可选:10分钟未访问则过期
                .recordStats()                  // 可选:开启统计
                .build();

        // 写入数据
        cache.put("key1", "A");
        cache.put("key2", "B");
        cache.put("key3", "C");
        System.out.println(cache.asMap()); // {key1=A, key2=B, key3=C}

        // 访问 key1(LRU 会将 key1 移到最近使用的位置)
        cache.getIfPresent("key1");

        // 插入新 key4,此时 size 将超过 3,最久未使用的 key2 会被淘汰
        cache.put("key4", "D");
        System.out.println(cache.asMap()); // {key3=C, key1=A, key4=D}

        // 查看统计信息
        System.out.println(cache.stats()); // 命中率等
    }
}

关键点

  • maximumSize(3) 自动实现 LRU(基于访问顺序)。
  • getIfPresent 不会自动加载,若需要自动加载可以用 LoadingCache 并实现 load 方法。
  • 淘汰策略是异步执行的,最终一致。

3. Caffeine 示例(更现代,性能更好)

java 复制代码
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;

import java.util.concurrent.TimeUnit;

public class CaffeineLRUExample {
    public static void main(String[] args) {
        // 创建最大容量为 3 的缓存,基于访问顺序(LRU 是其 W-TinyLFU 的一部分)
        Cache<String, String> cache = Caffeine.newBuilder()
                .maximumSize(3)
                .expireAfterAccess(10, TimeUnit.MINUTES) // 可选
                .recordStats()
                .build();

        cache.put("key1", "A");
        cache.put("key2", "B");
        cache.put("key3", "C");
        System.out.println(cache.asMap()); // {key1=A, key2=B, key3=C}

        // 访问 key1
        cache.getIfPresent("key1");

        // 插入 key4 -> 淘汰最久未使用的 key2
        cache.put("key4", "D");
        System.out.println(cache.asMap()); // {key3=C, key1=A, key4=D}

        // 统计信息
        System.out.println(cache.stats());
    }
}

Caffeine 特有的高级用法(异步加载、手动加载等):

java 复制代码
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
        .maximumSize(10_000)
        .expireAfterWrite(5, TimeUnit.MINUTES)
        .build(key -> createExpensiveGraph(key));

4. 如何验证 LRU 行为(访问顺序淘汰)

java 复制代码
// 两个库行为一致
cache.put("a", "1");
cache.put("b", "2");
cache.put("c", "3");
cache.getIfPresent("a");          // 将 a 变成最近使用
cache.put("d", "4");              // 容量超限,淘汰最久未使用的 b
assert cache.asMap().containsKey("b") == false;
assert cache.asMap().containsKey("a") == true;

5. 线程安全说明

  • Guava CacheCaffeine 返回的 Cache 实例都是线程安全的,内部使用了 ConcurrentHashMap 类似的并发控制,无需额外同步。
  • 可以直接在多线程环境中共享使用。

6. 何时用 Guava,何时用 Caffeine?

  • Guava:成熟稳定,依赖较广,适合老项目或团队已经使用 Guava。
  • Caffeine :性能更高(尤其是高并发读多写少场景),命中率更高(W-TinyLFU 算法),内存占用更低。推荐新项目使用 Caffeine

Spring 5+ 和 Spring Boot 2.x 已将 Caffeine 作为默认的 CacheManager 实现(替代 Guava)。

相关推荐
djBe17esS1 小时前
实战:Java 日志中打印服务器 IP,快速区分多服务器日志归属
java·服务器·tcp/ip
moonsea02032 小时前
2026.4.2
开发语言·c++·算法
woai33642 小时前
JVM学习-基础篇-垃圾回收
java·jvm·学习
七夜zippoe2 小时前
应用安全实践(一):常见Web漏洞(OWASP Top 10)与防护
java·前端·网络·安全·owasp
nFBD29OFC2 小时前
Spring Cloud生态地图——注册、配置、网关、负载均衡与可观测的组合拳
spring·spring cloud·负载均衡
Zzj_tju2 小时前
Java 从入门到精通(十一):异常处理与自定义异常,程序报错时到底该怎么处理?
java·开发语言
cpp_25012 小时前
P10376 [GESP202403 六级] 游戏
c++·算法·动态规划·题解·洛谷·gesp六级
智者知已应修善业2 小时前
【51单片机4个IO实现16按键可扩展独立按键64矩阵驱动显示矩阵原值】2023-5-8
c++·经验分享·笔记·算法·51单片机
hui-梦苑2 小时前
[GROMACS]模拟数据分析前轨迹文件生成-轨迹预处理
人工智能·算法·数据分析