Java LRU 与 LFU 算法及应用小结

 更新时间:2026年07月06日 09:53:54   作者:Full Stack Developme  
LRU和LFU是两种主要的缓存淘汰算法,LRU基于最近访问时间淘汰数据,LFU基于访问频率淘汰数据,下面就来详细的介绍一下Java LRU 与 LFU 算法及应用小结,感兴趣的可以了解一下

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

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

核心思想

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

Java实现方式

方式一:LinkedHashMap一行搞定(最推荐)

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;  // 容量超标时移除最老元素
    }
}

使用示例

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(面试常考)

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题就是典型。

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

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. 应用场景对比

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

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

① 直接使用成熟缓存框架

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

// 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就是这么做的。

到此这篇关于Java LRU 与 LFU 算法及应用小结的文章就介绍到这了,更多相关Java LRU LFU内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • java字符串与格式化输出的深入分析

    java字符串与格式化输出的深入分析

    本篇文章是对java字符串与格式化输出进行了详细的分析介绍,需要的朋友参考下
    2013-06-06
  • 在mybatis中去除多余的前缀或者后缀操作

    在mybatis中去除多余的前缀或者后缀操作

    这篇文章主要介绍了在mybatis中去除多余的前缀或者后缀操作。具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-11-11
  • SpringMVC的概念以及快速入门示例

    SpringMVC的概念以及快速入门示例

    这篇文章主要介绍了SpringMVC的概念以及快速入门示例,SpringMVC 已经成为目前最主流的MVC框架之一,它通过一套注解,让一个简单的 Java 类成为处理请求的控制器,而无须实现任何接口,需要的朋友可以参考下
    2023-05-05
  • SpringBoot创建maven多模块项目实战代码

    SpringBoot创建maven多模块项目实战代码

    本篇文章主要介绍了SpringBoot创建maven多模块项目实战代码,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-09-09
  • Springboot自定义mvc组件如何实现

    Springboot自定义mvc组件如何实现

    这篇文章主要介绍了Springboot自定义mvc组件如何实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-11-11
  • 一篇文章带你了解Maven的继承和聚合

    一篇文章带你了解Maven的继承和聚合

    这篇文章主要为大家介绍了Maven的继承和聚合,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助
    2022-01-01
  • Java调用第三方http接口的四种方式总结

    Java调用第三方http接口的四种方式总结

    这篇文章主要给大家介绍了关于Java调用第三方http接口的四种方式,在实际开发中我们经常会与第三方公司进行合作,接入第三方接口,文中给出了详细的代码实例,需要的朋友可以参考下
    2023-08-08
  • Java LinkedList集合功能实例解析

    Java LinkedList集合功能实例解析

    这篇文章主要介绍了Java LinkedList集合功能实例解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-04-04
  • 如何通过SpringBoot实现商城秒杀系统

    如何通过SpringBoot实现商城秒杀系统

    这篇文章主要介绍了如何通过SpringBoot实现商城秒杀系统,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-11-11
  • Springboot实现邮件发送功能

    Springboot实现邮件发送功能

    这篇文章主要为大家详细介绍了Springboot实现邮件发送功能,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-02-02

最新评论