【死磕 Java 基础】--- 我一口气自己就动手实现一个 LRU

2023-11-09

大家好,我是大明哥

个人网站:https://www.topjava.cn/


LRU,即 Least Recently Use ,直译为 “最近最少使用”。它是根据数据的历史访问记录来进行数据淘汰的,淘汰掉最先访问的数据,其核心思想是 如果数据最近被访问过,那么将来被访问的几率也会更加高

要实现 LRU,需要做到两点:

  • 查询出最近最晚使用的项
  • 给最近使用的项做一个标记

实现的方案有多种,这里小编主要介绍两种:

  1. LinkedHashMap
  2. 双向链表 + HashMap

LinkedHashMap 实现

利用 LinkedHashMap 的原因就在于 LinkedHashMap 是有序的,默认情况下是按照元素的添加顺序存储的,也可以调整为根据访问顺序来调整内部顺序(设置参数 accessOrder 进行调整),即最近读取的数据放在最前面,我们就是利用 LinkedHashMap 的这个特性来实现 LRU。先来一个简单的例子吧:

    public static void main(String[] args){
        Map<String,String> map = new LinkedHashMap(10,0.75f,true);

        map.put("1","a");
        map.put("2","b");
        map.put("3","c");
        map.put("4","d");

        System.out.println("原始顺序为:");
        for(Iterator<Map.Entry<String,String>> it = map.entrySet().iterator();it.hasNext();){
            System.out.print(it.next().getKey() + "    ");
        }
        System.out.println();

        map.get("2");

        System.out.println("访问 4 之后的顺序为:");
        for(Iterator<Map.Entry<String,String>> it = map.entrySet().iterator();it.hasNext();){
            System.out.print(it.next().getKey() + "    ");
        }
    }

运行结果:

原始顺序为:
1    2    3    4    
访问 4 之后的顺序为:
1    3    4    2  

更多关于 LinkedHashMap,请看这篇文章:图解集合6:LinkedHashMap

LinkedHashMap 实现 LRU 有两种方式,一种是继承 LinkedHashMap,一种是利用组合的方式,下面分别演示这两种情况。

继承 LinkedHashMap

采用继承的方式实现起来是非常简单的,因为 LinkedHashMap 本身就已经具备了 LRU 的特性,我们只需要实现一点:当容器中元素个数超过我们设定的容量后,删除第一个元素即可。同时由于 LinkedHashMap 本身不具备线程安全,我们需要确保他线程安全,这个也很简单,重写 LinkedHashMap 的 get()put() 方法即可,或者使用 Collections.synchronizedMap() 方法也可以。实现如下:

public class LRUCacheLinkedHashMap<K,V> extends LinkedHashMap<K,V> {

    /**
     * 定一缓存容量
     */
    private int capacity;

    LRUCacheLinkedHashMap(int capacity){
        // AccessOrder = true
        super(capacity,0.75f,true);

        this.capacity = capacity;
    }

    /**
     * 实现LRU的关键方法,如果 map 里面的元素个数大于了缓存最大容量,则删除链表的顶端元素
     *
     * @param eldest
     * @return
     */
    @Override
    public boolean removeEldestEntry(Map.Entry<K, V> eldest){
        System.out.println(eldest.getKey() + "=" + eldest.getValue());
        return size()>capacity;
    }

    @Override
    public synchronized V get(Object key) {
        return super.get(key);
    }

    @Override
    public synchronized V put(K key, V value) {
        return super.put(key, value);
    }
}

验证

   public static void main(String[] args){
        LRUCacheLinkedHashMap cache = new LRUCacheLinkedHashMap(5);

        cache.put("1","a");
        cache.put("2","b");
        cache.put("3","c");
        cache.put("4","d");
        cache.put("5","e");

        System.out.println("插入 5 个元素后的顺序");
        printlnCache(cache);

        // 插入第 6 个元素
        cache.put("6","e");

        System.out.println("插入第 6 个元素后的顺序");
        printlnCache(cache);

        // 访问 第 3 个元素
        cache.get("3");

        System.out.println("访问元素 3 后的顺序");
        printlnCache(cache);

    }

    private static void printlnCache(LRUCacheLinkedHashMap cacheMap){
        for(Iterator<Map.Entry<String,String>> it = cacheMap.entrySet().iterator(); it.hasNext();){
            System.out.print(it.next().getKey() + "    ");
        }
        System.out.println();
    }

运行结果:

插入 5 个元素后的顺序
1    2    3    4    5    
插入第 6 个元素后的顺序
2    3    4    5    6    
访问元素 3 后的顺序
2    4    5    6    3 

运行结果完全符合我们的预期

组合 LinkedHashMap

使用组合的方式可能会更加优雅些,但是由于没有实现 Map 接口,所以就不能使用 Collections.synchronizedMap() 方式来保证线程安全性了,所以需要在每个方法处增加 synchronized 来确保线程安全。实现方式如下:

public class LRUCache<K,V> {
    private int capacity;

    private Map<K,V> cacheMap;

    public LRUCache(int capacity){
        this.capacity = capacity;

        cacheMap = new LinkedHashMap<>(capacity,0.75f,true);
    }

    public synchronized void put(K k,V v){
        cacheMap.put(k,v);

        // 移除第一个元素
        if(cacheMap.size() > capacity){
            K first = this.keyIterator().next();

            cacheMap.remove(first);
        }
    }

    public synchronized V get(K k){
        return cacheMap.get(k);
    }

    public Iterator<K> keyIterator(){
        return cacheMap.keySet().iterator();
    }
}

验证:

    public static void main(String[] args) {
        LRUCache lruCache = new LRUCache(5);

        lruCache.put("1","a");
        lruCache.put("2","b");
        lruCache.put("3","c");
        lruCache.put("4","d");
        lruCache.put("5","e");

        System.out.println("插入 5 个元素后的顺序");
        println(lruCache);

        // 插入第 6 个元素
        lruCache.put("6","e");

        System.out.println("插入 第 6 个元素后的顺序");
        println(lruCache);

        // 访问 第 3 个元素
        lruCache.get("3");

        System.out.println("访问元素 3 后的顺序");
        println(lruCache);

    }

    private static void println(LRUCache lruCache){
        for(Iterator it = lruCache.keyIterator(); it.hasNext();){
            System.out.print(it.next() + "    ");
        }
        System.out.println();
    }

运行结果如下:

插入 5 个元素后的顺序
1    2    3    4    5    
插入 第 6 个元素后的顺序
2    3    4    5    6    
访问元素 3 后的顺序
2    4    5    6    3 

组合的方式也显得非常简单,有两点需要注意:

  1. 保证每个方法的线程安全
  2. put 时,需要查看当前容量是否超过设置的容量,超过则需要删除第一个元素。当然小编这种是实现方式不是很优雅,这么做知识为了能够更加好阐述 LRU 的实现。更好的方案是在构造 LinkedHashMap 时,重写 removeEldestEntry(),如下:
        cacheMap = new LinkedHashMap<K,V>(capacity,0.75f,true){
            @Override
            protected boolean removeEldestEntry(Map.Entry eldest) {
                return size()>capacity;
            }
        };

链表 + HashMap 实现

我们想想,在不利用现存数据结构的条件(如 LinkedHashMap)如何实现一个 LRU 呢?缓存部分容易实现,我们都知道利用 HashMap 即可,但是如何实现缓存容量不足时丢弃最不常用的数据的功能?

  • 利用时间戳。每一个访问,增加的元素我们都给其更新一个时间戳,在 put 的时候,检查,删除时间戳最小的就可以了。这种方法可以实现,但是代价较高,就是我们需要遍历整个数据,得到最小的时间戳。
  • 我们可以换位思考,我们其实不需要关注每个节点的增加或者遍历时间,我们只需要知道那个节点是最先访问就可以了,所以我们可以利用链表记录访问记录,有新数据加入时放在链表的 head 节点,每次访问也将该数据放在 head 节点,那么链表的 tail 一定是最早访问的节点,所以每次当容量不足的时候删除 tail 节点数据并将它的前驱节点设置为 tail 就可以了。注意,这个链表是一个双向链表。代码如下:
public class LinkedLRUCache<K,V> {

    private int capacity;

    private Map<K,LRUNode> map;

    private LRUNode head;

    private LRUNode tail;

    LinkedLRUCache(int capacity){
        this.capacity = capacity;
        this.map = new HashMap<>();
    }

    public synchronized void put(K k,V v){
        LRUNode node = map.get(k);

        // 存在该 key,将节点的设置为 head
        if(node != null){
            node.value = v;

            remove(node,false);
        }else{
            /**
             * 该节点不存在
             * 1、将该节点加入缓存
             * 2、设置该节点为 head
             * 3、判断是否超出容量
             */
            node = new LRUNode(k,v);

            if(map.size() >= capacity){
                //删除 tail 节点
                remove(tail,true);
            }

            map.put(k,node);

            setHead(node);
        }

        // 设置当前节点为首节点
        setHead(node);
    }

    public Object get(String key) {
        LRUNode node = map.get(key);
        if (node != null) {
            // 将刚操作的元素放到head
            remove(node, false);
            setHead(node);
            return node.value;
        }
        return null;
    }

    /**
     * 设置头结点
     *
     * @param node
     */
    private void setHead(LRUNode node) {
        if(head != null){
            node.next = head;
            head.prev = node;
        }

        head = node;

        if(tail == null){
            tail = node;
        }
    }

    /**
     * 从链表中删除此Node
     *
     * @param node
     * @param flag  为 true 就删除该节点的 key
     */
    private void remove(LRUNode node,boolean flag) {
        if (node.prev != null) {
            node.prev.next = node.next;
        } else {
            head = node.next;
        }
        if (node.next != null) {
            node.next.prev = node.prev;
        } else {
            tail = node.prev;
        }
        node.next = null;
        node.prev = null;
        if (flag) {
            map.remove(node.key);
        }
    }
    
    private Iterator iterator(){
        return map.keySet().iterator();
    }

    private class LRUNode<K,V> {

        /**
         * cache 的 key
         */
        private K key;

        /**
         * cache 的 value
         */
        private V value;

        private LRUNode next;

        private LRUNode prev;

        LRUNode(K key, V value) {
            this.key = key;
            this.value = value;
        }
    }
}

验证

   public static void main(String[] args){
        LRUCache lruCache = new LRUCache(5);
        
        lruCache.put("1","a");
        lruCache.put("2","b");
        lruCache.put("3","c");
        lruCache.put("4","d");
        lruCache.put("5","e");
       
        System.out.println("插入 5 个元素");
        println(lruCache);

        System.out.println("插入 3 元素");
        lruCache.put("3","c");
        println(lruCache);

        System.out.println("插入第  6 个元素");
        lruCache.put("6","f");
        println(lruCache);

        System.out.println("访问 4 元素");
        lruCache.get("4");
        println(lruCache);
    }
    
    private static void println(LRUCache lruCache){
        Iterator iterator = lruCache.keyIterator();
        while (iterator.hasNext()){
            System.out.print(iterator.next() + "    ");
        }
        
        System.out.println();
    }

执行结果:

插入 5 个元素
1    2    3    4    5    
插入 3 元素
1    2    4    5    3    
插入第  6 个元素
2    4    5    3    6    
访问 4 元素
2    5    3    6    4 
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【死磕 Java 基础】--- 我一口气自己就动手实现一个 LRU 的相关文章

  • C# 创建Excel并写入内容

    在许多应用程序中 需要将数据导出为Excel表格 以便用户可以轻松地查看和分析数据 在本文中 我们将讨论如何使用C 创建Excel表格 并将数据写入该表格 添加引用 在C 中创建Excel表格 需要使用Microsoft Office In
  • Typora和PicGo-Core搭配使用(解决博客单独上传图片问题)

    前言 本文简单介绍快速上传图片并获取图片 URL 链接的工具 图片存放到Gitee仓库中 在博客网站发布时不必担心图片转存失败问题 解决本地图片在网站需单独上传的难题 将本地图片存储在网络中 图床 并生成URL 联网情况下通过URL链接即可
  • Unity之六:项目实战篇

    文章目录 一 一个简单的实例 二 使用CMake组织项目与Unity 2 1 目录结构 2 2 CMakeLists txt的编写 2 3 使用实例 一 一个简单的实例 一个测试单元是源文件 测试文件和Unity构成的 把他们放在一起进行编
  • 【算法提升】——异或理解,位的运算

    个人主页 努力学习的少年 版权 本文由 努力学习的少年 原创 在CSDN首发 需要转载请联系博主 如果文章对你有帮助 欢迎关注 点赞 收藏 一键三连 和订阅专栏哦 目录 一 只出现一次的数字 1 二 数组中只出现一次的数字2 一 只出现一次
  • localStorage在Safari浏览器无痕模式下失效

    Safari无痕模式是不能使用localStorage的 可以利用这个特性判断用户是否开启无痕模式 并提醒用户关闭无痕模式 if typeof localStorage object try localStorage setItem loc
  • 学习笔记 JavaScript ES6 异步编程Promise

    Promise ES里面对异步操作的第一种方案 学习Promise 让异常操作变得优雅 Promise的精髓在于异步操作的状态管理 一个Promise最基本用法 他的参数是一个方法 这个方法里有两个参数 一个是异步操作执行成功的回调 一个是
  • DC综合脚本中文详细解释

    script for Design Compiler DC综合编译脚本 language TCL 语言说明 Usage 使用说明 1 make sure the lib in the current directory 确保设计库在正确的文
  • Xcode项目设置项中的LLVM

    LLVM是构架编译器
  • html5开发手机打电话发短信功能,html5的高级开发,html5开发大全,html手机电话短信功能详解

    原文地址 http blog csdn net xmtblog article details 32931905 在很多的手机网站上 有打电话和发短信的功能 对于这些功能是如何实现的呢 其实不难 今天我们就用html5来实现他们 简单的让你
  • Angular--官方文档之 Angular CLI

    学习Angular官方文档的时候 参考https angular cn guide quickstart 这个快速开发的文档 对于我这个AngularJs小白在看了Angular菜鸟教程后 只能说可以简单的运用一下 看到一些专业术语 我也是
  • 嵌入式Linux(四)—嵌入式C语言(杂项/数据类型关键字)

    目录 杂项关键字 sizeof Return 数据类型关键字 char 进制 int long short Unsigned signed Float double void 自定义数据类型 Struct Union enum typede
  • cppcheck使用

    cppcheck使用 cppcheck说明 cppcheck能够检查出来的问题 cppcheck使用并生成html结果 生成html结果 cppcheck说明 cppcheck主要用来检查c c 代码的 本文主要讲述cppcheck用命令行

随机推荐

  • Flutter 开发小结

    接触 Flutter 已经有一阵子了 期间记录了很多开发小问题 苦于忙碌没时间整理 最近项目进度步上正轨 借此机会抽出点时间来统一记录这些问题 并分享项目开发中的一点心得以及多平台打包的一些注意事项 希望能对大家有所帮助 UI 组件使用 官
  • Linux 下使用Crontab定时任务同时执行多条定时任务

    Linux 下使用Crontab定时任务同时执行多条定时任务 使用 符连接即可 示例如下 0 6 bea ceos timer bin pb ClosePbManifestTimer sh gt dev null 2 gt 1 bea ce
  • 【高项】质量管理(ITTO)

    过程组 子过程 输入 I 工具和技术 TT 输出 O 规划 1规划质量管理 1 项目章程 2 项目管理计划 需求管理计划 风险管理计划 相关方参与计划 范围基准 3 项目文件 假设日志 需求文件 需求跟踪矩阵 风险登记册 相关方登记册 4
  • QT源码剖析-QT对象通信机制信号槽的绑定具体实现

    本文详细介绍QT核心机制之一 信号和槽 我们在此根据Qt源代码一步一步探究其信号槽的实现过程 核心知识点 模板元编程技术 Qt moc预编译机制 QObject类 目录 1 QObject类介绍 2 相关助手类介绍 2 1 类型 函数指针
  • pip安装出现Could not install packages due to an EnvironmentError: [Errno 2] No such file or directory: '

    问题描述 pip安装库或者更新pip版本时出现如下问题 Could not install packages due to an EnvironmentError Errno 2 No such file or directory c us
  • LeetCode 面试题01.09 字符串轮转

    题目 字符串轮转 给定两个字符串s1和s2 请编写代码检查s2是否为s1旋转而成 比如 waterbottle 是 erbottlewat 旋转后的字符串 示例1 输入 s1 waterbottle s2 erbottlewat 输出 Tr
  • 一个独特的开源插件evil.js

    前言 最近发现一个好玩有解压的开源插件 注意 不可使用在正式项目中 这里分享下 gitee地址 evil js 此代码仅在周日的时候执行以下逻辑 声明 请勿用于任何项目 如果导致任何问题 与本人无关https gitee com haoxi
  • 矩阵LU分解

    一 矩阵LU分解定理 设A为n阶矩阵 如果A的顺序主子式Di 0 i 1 2 n 1 则A可以分解为一个单位下三角矩阵L和一个上三角矩阵U的乘积 且这种分解是唯一的 即A LU 二 矩阵LU分解Python代码 自己原创 def lu de
  • 第十二章 - 条件判断(case when 和 if)和视图

    第十二章 条件判断 case when 和 if 和视图 view if 的用法 case when 的用法 视图 view 的用法 if 的用法 通过使用if函数可以实现数据二分类或者多分类的功能 比如按年龄区分青年 中年 老年 或者按价
  • Python2_Pandas库(数据读取)

    1 数据读取 food info csv数据 import pandas food info pandas read csv food info csv read csv函数读取csv数据文件 print type food info Da
  • 汇编笔记——判断大小

    判断指令 CMP AL num 判断条件 这里的JA JB JE JMP相当于goto命令 JA L0 A gt above AL比num大 执行L0 JB L1 B gt below AL比num小 执行L1 JE L2 E gt equ
  • 树结构转List

    使用LinkedList效率更高 1 单个顶级节点 public static List
  • 网络安全(黑客技术)自学笔记

    目录 一 自学网络安全学习的误区和陷阱 二 学习网络安全的一些前期准备 三 网络安全学习路线 四 学习资料的推荐 想自学网络安全 黑客技术 首先你得了解什么是网络安全 什么是黑客 网络安全可以基于攻击和防御视角来分类 我们经常听到的 红队
  • chromium之jumplist

    chrome在win7及之后系统添加jumplist功能 jumplist即系统任务栏相关的功能 包括任务栏图标 鼠标放置后视图 进度条 右键菜单等等 路径 chromium src chrome browser win jumplist
  • 21. 合并两个有序链表

    21 合并两个有序链表 简单 将两个升序链表合并为一个新的 升序 链表并返回 新链表是通过拼接给定的两个链表的所有节点组成的 输入 l1 1 2 4 l2 1 3 4 输出 1 1 2 3 4 4 示例 2 输入 l1 l2 输出 示例 3
  • vue 阻止事件冒泡,捕获方法

    要想了解 VUE 阻止事件冒泡和捕获方法 首先要了解一下 JS 事件和 JS 阻止事件冒泡 捕获方法 1 js 事件的三阶段 捕获阶段 目标阶段 执行当前对象的事件处理程序 冒泡阶段 2 js 阻止事件冒泡 捕获 阻止事件冒泡 event
  • OceanBase 安全审计之透明加密

    承接前文 OceanBase 安全审计的 传输加密 本文主要实践数据透明加密 并验证加密是否有效 作者 张乾 外星人2号 兼任四位喵星人的铲屎官 爱可生开源社区出品 原创内容未经授权不得随意使用 转载请联系小编并注明来源 本文约 1200
  • layui导入Excel文件

    具体如下图所示 首先 导入layui第三方插件js 地址 https fly layui com extend excel 1 在页面中引入excel js文件 引入excel layui config base layui ext ext
  • NOIP 1998 普及组 复赛 幂次方

    NOIP 1998 普及组 复赛 幂次方 1208 2的幂次方表示 此文代码与本人极其相似 唯一不同就是此文代码成功了 http www cnblogs com bofengyu p 4477355 html 思路 先打印2 7 2 3 2
  • 【死磕 Java 基础】--- 我一口气自己就动手实现一个 LRU

    大家好 我是大明哥 个人网站 https www topjava cn LRU 即 Least Recently Use 直译为 最近最少使用 它是根据数据的历史访问记录来进行数据淘汰的 淘汰掉最先访问的数据 其核心思想是 如果数据最近被访