Java中自定义LRU缓存详解

 更新时间:2023年09月21日 10:45:52   作者:晓之木初  
这篇文章主要介绍了Java中自定义LRU缓存详解,基于LRU算法的缓存系统,可以在达到缓存容量上限时,清理最近最少使用的数据,为新的数据的插入腾出空间,需要的朋友可以参考下

1. LRU算法

  • 在计算机领域,LRU算法的应用非常多,最常见的就是LRU缓存
  • LRU:Least Recently USed,最近最少使用
  • 英文和中文存在差异,如果只看中文,貌似RLU更合适
  • 基于LRU算法的缓存系统,可以在达到缓存容量上限时,清理最近最少使用的数据,为新的数据的插入腾出空间
  • leetcode上,也有对应的LRU缓存算法题:146. LRU 缓存机制
  • 牛客上,蚂蚁金服的面试题库,LRU缓存也赫然在列
  • 题目要求大概如下:
    • 设计和实现一个LRU算法的缓存数据结构。需要实现两个操作:get和set
    • 获取数据 get(key) :如果关键字 (key) 存在于缓存中,则获取关键字的值(总是正数),否则返回 -1。
    • 写入数据 put(key, value) :
      • 如果关键字已经存在,则变更其数据值;
      • 如果关键字不存在,则插入该组<key, value>。
      • 当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。

2. 继承LinkedHashMap实现LRU缓存

通过对LinkedHashMap的学习,我们了解到:

与HashMap不同,LinkedHashMap作为链表形式的哈希表,支持元素的插入顺序或访问顺序

使用访问顺序时,通过重写removeEldestEntry()方法,可以删除最近最少使用的键值对

因此,可以通过继承LinkedHashMap、重写removeEldestEntry() 方法,实现LRU缓存

import java.util.LinkedHashMap;
import java.util.Map;
public class LRUCache extends LinkedHashMap<Integer, Integer> {
    private int capacity;
    public LRUCache(int capacity) {
        // true表示按访顺序存储键值对,最近访问的在尾部,最近最少访问在头部
        super(capacity, 0.75f, true);
        this.capacity = capacity;
    }
    @Override
    protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
        return size() > capacity;
    }
    public int get(int key) {
        // 根据题目要求,不存在key时,不能直接返回null值,而是需要返回默认值-1
        return super.getOrDefault(key, -1);
    }
    public void put(int key, int value) {
        super.put(key, value);
    }
}

3. 自定义LRU缓存

  • 如果在面试时,碰到该题,应该先和面试官确认,是否能使用现成的数据结构去实现
  • 如果面试官明确要求说,需要自己去实现LRU缓存,不能使用现成的数据结构,那么时候展现你的实力了

最原始的想法:

  • 既然LinkedHashMap都是双向链表 + HashMap实现的,那我自己定义一个双向链表实现类似的功能
class DLinkedNode{
    private int key;
    private int val;
    private DLinkNode prev;
    private DLinkNode next;
}
  • 基于双向链表,可以在 O ( 1 ) O(1) O(1)的时间内快速增加、删除节点
  • 但在确定节点位置时,需要从头到尾遍历链表
  • 就算使用双指针左右开弓,在定位节点时,依然存在很大的开销

思路进阶

  • 借助HashMap,以O ( 1 ) O(1)O(1)的时间复杂度快速get或put数据
  • 同时,规定:最近访问的节点放在链表的头部,最近最少访问的节点放在链表的尾部
  • 如果头部或尾部直接存储数据,则在实现时需要考虑节点是否为头节点或尾结点的情况
  • 因此,直接创建dummy的head和tail节点,以减少编写代码的工作量

最终的代码实现

通过上述分析,代码如下

import java.util.HashMap;
public class LRUCache{
    private HashMap<Integer, DLinkedNode> map;
    private DLinkedNode head;
    private DLinkedNode tail;
    private int capacity;
    private int size;
    public LRUCache(int capacity) {
        this.capacity = capacity;
        map = new HashMap<>();
        // 初始化带dummy节点的双向链表
        head = new DLinkedNode();
        tail = new DLinkedNode();
        head.next = tail;
        tail.prev = head;
    }
    public int get(int key) {
        DLinkedNode node = map.get(key);
        if (node == null) {
            return -1;
        }
        // 被访问,需要从当前位置移动到头部
        if (head.next != node){
            removeNode(node);
            insertToHead(node);
        }
        return node.val;
    }
    public void put(int key, int value) {
        DLinkedNode node = map.get(key);
        // 如果存在,直接更新值
        if (node != null) {
            node.val = value;
            // 被访问,需要从当前位置移动到头部
            if (head.next != node) {
                removeNode(node);
                insertToHead(node);
            }
        } else {
            // 插入前,先判断是否需要腾出空间
            if (size == capacity) {
                // 从链表中删除尾结点
                DLinkedNode last = tail.prev;
                removeNode(last);
                // 从map中移除记录
                map.remove(last.key);
                size--;
            }
            // 新建节点并插入
            DLinkedNode newNode = new DLinkedNode(key, value);
            insertToHead(newNode);
            map.put(key, newNode);
            size++;
        }
    }
    // 插入节点一定是在头部
    public void insertToHead(DLinkedNode node) {
        // 分别建立与head.next和head的关联
        DLinkedNode next = head.next;
        node.next = next;
        next.prev = node;
        head.next = node;
        node.prev = head;
    }
    // 删除指定节点
    public void removeNode(DLinkedNode node) {
        DLinkedNode prev = node.prev;
        DLinkedNode next = node.next;
        prev.next = next;
        next.prev = prev;
        // 断开引用,帮助GC
        node.prev = null;
        node.next = null;
    }
}
class DLinkedNode{
     int key;
     int val;
     DLinkedNode prev;
     DLinkedNode next;
    public DLinkedNode() {
    }
    public DLinkedNode(int key, int val) {
        this.key = key;
        this.val = val;
    }
}

几点注意事项:

  • 节点移动到头部情况:get时,节点被访问;put时,节点的值被更新。put时的情况,容易被忽略
  • 新增节点时,按照题目要求是先删除最久未使用的节点,并非先插入再删除
  • 注意双向链表和HashMap的联动,删除或新增节点,HashMap中也要删除或新增记录

到此这篇关于Java中自定义LRU缓存详解的文章就介绍到这了,更多相关Java的LRU缓存内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • IDEA消除指定警告的两种方法小结

    IDEA消除指定警告的两种方法小结

    有时候IDEA会代码中给出一些我们不需要的警告,看起来就很不美观,本文主要介绍了IDEA消除指定警告的两种方法,感兴趣的可以了解一下
    2023-08-08
  • Spring中的StopWatch记录操作时间代码实例

    Spring中的StopWatch记录操作时间代码实例

    这篇文章主要介绍了Spring中的StopWatch记录操作时间代码实例,spring-framework提供的一个StopWatch类可以做类似任务执行时间控制,也就是封装了一个对开始时间,结束时间记录操作的Java类,需要的朋友可以参考下
    2023-11-11
  • SpringMVC 拦截器的使用示例

    SpringMVC 拦截器的使用示例

    这篇文章主要介绍了SpringMVC 拦截器的使用示例,帮助大家更好的理解和学习使用SpringMVC,感兴趣的朋友可以了解下
    2021-04-04
  • JavaSE实现图书管理系统的示例代码

    JavaSE实现图书管理系统的示例代码

    这篇博客是在学习了一部分Java基础语法之后的练习项目,通过这个小项目的练习,对Java中的类和对象,抽象类和接口等进行熟悉理解。快跟随小编一起学习学习吧
    2022-08-08
  • 深入讲解spring boot中servlet的启动过程与原理

    深入讲解spring boot中servlet的启动过程与原理

    这篇文章主要给大家介绍了关于spring boot中servlet启动过程与原理的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2018-07-07
  • maven grpc整合springboot demo

    maven grpc整合springboot demo

    这篇文章主要为大家介绍了基于maven grpc整合springboot demo,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-04-04
  • 浅谈SpringMVC HandlerInterceptor诡异问题排查

    浅谈SpringMVC HandlerInterceptor诡异问题排查

    这篇文章主要介绍了浅谈SpringMVC HandlerInterceptor诡异问题排查,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2019-05-05
  • SpringCloud中的Hystrix保护机制详解

    SpringCloud中的Hystrix保护机制详解

    这篇文章主要介绍了SpringCloud中的Hystrix保护机制详解,Hystrix,英文意思是豪猪,全身是刺,看起来就不好惹,是一种保护机制,Hystrix也是Netflix公司的一款组件,需要的朋友可以参考下
    2023-12-12
  • springboot中事务管理@Transactional的注意事项与使用场景

    springboot中事务管理@Transactional的注意事项与使用场景

    今天小编就为大家分享一篇关于springboot中事务管理@Transactional的注意事项与使用场景,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2019-04-04
  • spring中jdbcTemplate.batchUpdate的几种使用情况

    spring中jdbcTemplate.batchUpdate的几种使用情况

    本文主要介绍了spring中jdbcTemplate.batchUpdate的几种使用情况,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-04-04

最新评论