Java LRU 与 LFU 算法及应用

在Java面试和日常开发中,LRULFU 是出现频率最高的两种缓存淘汰算法。它们的核心区别在于淘汰依据:LRU看"时间"(最近谁没用过),LFU看"频率"(谁最不常用)


1. LRU(Least Recently Used,最近最少使用)

核心思想

淘汰最长时间未被访问的数据。假设"如果数据最近被访问过,那么将来被访问的概率也更高"。

Java实现方式

方式一:LinkedHashMap 一行搞定(最推荐)
java 复制代码
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private final int maxCapacity;

    public LRUCache(int capacity) {
        // accessOrder=true 表示按访问顺序排序
        super(capacity, 0.75f, true);
        this.maxCapacity = capacity;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > maxCapacity;  // 容量超标时移除最老元素
    }
}

使用示例

java 复制代码
LRUCache<Integer, String> cache = new LRUCache<>(3);
cache.put(1, "A");
cache.put(2, "B");
cache.put(3, "C");
cache.get(1);        // 访问1,使其变为最新
cache.put(4, "D");   // 触发淘汰,移除最久未用的2
System.out.println(cache); // {3=C, 1=A, 4=D}
方式二:手写双向链表 + HashMap(面试常考)
java 复制代码
class LRUCacheManual {
    class DLinkedNode {
        int key, value;
        DLinkedNode prev, next;
        DLinkedNode() {}
        DLinkedNode(int key, int value) { this.key = key; this.value = value; }
    }

    private Map<Integer, DLinkedNode> cache = new HashMap<>();
    private int size, capacity;
    private DLinkedNode head, tail;

    public LRUCacheManual(int capacity) {
        this.capacity = capacity;
        head = new DLinkedNode();
        tail = new DLinkedNode();
        head.next = tail;
        tail.prev = head;
    }

    public int get(int key) {
        DLinkedNode node = cache.get(key);
        if (node == null) return -1;
        moveToHead(node);  // 移到链表头部(表示最近使用)
        return node.value;
    }

    public void put(int key, int value) {
        DLinkedNode node = cache.get(key);
        if (node != null) {
            node.value = value;
            moveToHead(node);
        } else {
            DLinkedNode newNode = new DLinkedNode(key, value);
            cache.put(key, newNode);
            addToHead(newNode);
            size++;
            if (size > capacity) {
                DLinkedNode tail = removeTail();
                cache.remove(tail.key);
                size--;
            }
        }
    }
    // 省略 addToHead, removeTail, moveToHead 等链表操作...
}

2. LFU(Least Frequently Used,最不经常使用)

核心思想

淘汰访问次数最少的数据。如果访问次数相同,则淘汰最久未使用的(需要结合时间)。

实现难点

LFU比LRU复杂得多,因为需要同时维护频率时间两个维度。LeetCode 460题就是典型。

经典实现(双哈希表 + 频率链表)

java 复制代码
class LFUCache {
    private Map<Integer, Node> keyToNode;    // key -> 数据节点
    private Map<Integer, LinkedHashSet<Node>> freqToKeys; // 频率 -> 该频率下的key集合(用LinkedHashSet保证顺序)
    private int minFreq;  // 当前最小频率
    private int capacity;

    class Node {
        int key, value, freq;
        Node(int key, int value) {
            this.key = key;
            this.value = value;
            this.freq = 1;  // 初始频率为1
        }
    }

    public LFUCache(int capacity) {
        this.capacity = capacity;
        keyToNode = new HashMap<>();
        freqToKeys = new HashMap<>();
        minFreq = 0;
    }

    public int get(int key) {
        if (!keyToNode.containsKey(key)) return -1;
        Node node = keyToNode.get(key);
        updateFreq(node);  // 访问后频率+1
        return node.value;
    }

    public void put(int key, int value) {
        if (capacity == 0) return;
        if (keyToNode.containsKey(key)) {
            Node node = keyToNode.get(key);
            node.value = value;
            updateFreq(node);
        } else {
            if (keyToNode.size() >= capacity) {
                // 淘汰最小频率中最久未使用的那个
                LinkedHashSet<Node> set = freqToKeys.get(minFreq);
                Node oldest = set.iterator().next();
                set.remove(oldest);
                keyToNode.remove(oldest.key);
                if (set.isEmpty()) freqToKeys.remove(minFreq);
            }
            Node newNode = new Node(key, value);
            keyToNode.put(key, newNode);
            freqToKeys.computeIfAbsent(1, k -> new LinkedHashSet<>()).add(newNode);
            minFreq = 1;  // 新插入的一定是频率最低
        }
    }

    private void updateFreq(Node node) {
        int oldFreq = node.freq;
        // 从旧频率集合中移除
        LinkedHashSet<Node> oldSet = freqToKeys.get(oldFreq);
        oldSet.remove(node);
        if (oldSet.isEmpty()) {
            freqToKeys.remove(oldFreq);
            if (minFreq == oldFreq) minFreq++;
        }
        // 放入新频率集合
        node.freq++;
        freqToKeys.computeIfAbsent(node.freq, k -> new LinkedHashSet<>()).add(node);
    }
}

3. 应用场景对比

维度 LRU LFU
适用场景 热点数据有明显时效性(如用户最近浏览) 热点数据稳定且长期(如热门视频、全局配置)
典型例子 Redis内存淘汰(allkeys-lru)、Glide图片缓存、浏览器后退按钮 数据库查询缓存(统计高频SQL)、CDN内容缓存
优点 实现简单,性能高,符合局部性原理 能过滤掉偶发性突发流量,保留真正高频数据
缺点 容易被批量访问"污染"(一次全表扫描会把所有旧数据挤出去) 实现复杂,维护成本高;新数据刚进来频率低,容易被立即淘汰(需加老化机制)

4. 生产环境中的最佳实践

① 直接使用成熟缓存框架

不要自己写LFU,99%的场景用Google的Caffeine就够了:

java 复制代码
// Caffeine 同时支持 LRU 和 LFU(实际是 TinyLFU,LFU的增强版)
Cache<String, Object> cache = Caffeine.newBuilder()
    .maximumSize(1000)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .recordStats()
    .build();

Caffeine使用的TinyLFU(论文算法)结合了LFU和LRU的优点,并解决了新数据"饥饿"问题。

② Redis中的使用

  • LRU :Redis默认近似LRU(非精确,采样淘汰),配置maxmemory-policy allkeys-lru

  • LFU :Redis 4.0+支持,配置allkeys-lfu,适合稳定的热点数据缓存

③ 数据库连接池或线程池

使用LinkedBlockingDeque实现简单LRU,淘汰长时间空闲的连接。


5. 面试高频追问

Q:为什么LinkedHashMap能实现LRU?

A:构造参数accessOrder=true使链表按访问顺序排序,每次get/put都会将节点移到尾部,头部就是最久未用的。重写removeEldestEntry控制淘汰时机。

Q:LRU和LFU哪个更优?

A:没有绝对优劣。突发流量场景 (如双11秒杀)用LRU更合适;稳态热点场景(如排行榜)用LFU更精准。工业界常用两者混合(如Arc算法、TinyLFU)。

Q:如何解决LFU中"新数据刚来就被淘汰"的问题?

A:给新数据初始频率赋一个较高的值(如初始频率=5),或使用窗口计数(只统计最近一段时间内的访问次数),Caffeine的TinyLFU就是这么做的。

相关推荐
代码方舟8 小时前
O2O二手车交易平台B端车商管理后台:基于天远车信盟出险构建自动化车况评估网关
java·运维·人工智能·自动化
爱学堂IT分享8 小时前
图灵Java互联网架构师六期
java·开发语言
坐吃山猪8 小时前
【多线程】Semaphore使用
java·数据库·oracle·多线程
传奇开心果编程8 小时前
【Rust入门知识点学与练】第3课:String 与 &str 的区别
开发语言·学习·rust
秋名RG8 小时前
Java 核心特性一览
java·开发语言
霸道流氓气质8 小时前
Spring AI 技术细节:RAG QuestionAnswerAdvisor 设计与实现
java·人工智能·spring
平头哥AI9 小时前
Day 01 | go run 跑通第一个 Go 程序,go build 留下一个能拷走的 exe
开发语言·后端·golang
斑马1399 小时前
Linux软件编程学习笔记(十二)——TCP并发服务器模型
java·服务器·网络
冬栈程序设计9 小时前
Springboot宿舍管理系统【670099】 -附源码(开箱即用)
java·毕业设计·springboot·课程设计·程序设计·大作业
2601_962284509 小时前
北京Java全栈开发培训 Web前端开发 软件测试就业培训班
java·软件测试·web前端·全栈开发·就业培训