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)。

相关推荐
csdn_aspnet6 小时前
C语言 Lomuto分区算法(Lomuto Partition Algorithm)
c语言·开发语言·算法
Dicky-_-zhang6 小时前
消息队列Kafka/RocketMQ选型与高可用架构:从单体到100万TPS的演进
java·jvm
晨曦中的暮雨6 小时前
4.15腾讯 CSIG云服务产线 一面
java·开发语言
fake_ss1987 小时前
AI时代学习全栈项目开发的新范式
java·人工智能·学习·架构·个人开发·学习方法
谙弆悕博士7 小时前
【附C源码】从零实现C语言堆数据结构:原理、实现与应用
c语言·数据结构·算法··数据结构与算法
茉莉玫瑰花茶7 小时前
工作流的常见模式 [ 1 ]
java·服务器·前端
未若君雅裁7 小时前
Spring AOP、日志切面与声明式事务原理
java·后端·spring
No8g攻城狮8 小时前
【人大金仓】wsl2+ubuntu22.04安装人大金仓数据库V9
java·数据库·spring boot·非关系型数据库
xiaoerbuyu12338 小时前
开源Java 邮箱 基于SpringBoot+Vue前后端分离的电子邮件
java·开发语言