如何在Java中实现LRU缓存(手撕,算法)

一、概述

最近最少使用(LRU)缓存是一种缓存回收算法,它按照使用顺序组织元素。在LRU中,顾名思义,最长时间未使用的元素将从该高速缓存中被逐出。在本教程中,我们将学习LRU,并学习Java中的实现。

例如,如果我们有一个容量为三个项目的缓存:

最初,该cache是空的,我们将元素8放入该高速缓存。元素9和6像以前一样被缓存。但是现在,该高速缓存容量已满,为了放入下一个元素,我们必须丢弃该高速缓存中最近最少使用的元素。

在我们用Java实现LRU缓存之前,最好先了解一下该高速缓存的一些方面:

  • 所有的操作都应该按照O(1)的顺序运行
  • 所有缓存操作都必须支持并发性
  • 若该高速缓存已满,则添加新项必须调用LRU策略

二、LRU的结构

需要设计一个数据结构,可以在恒定时间内完成查询、排序(按时间排序)和删除元素等操作?

为了解决这个问题,我们需要深入思考一下关于LRU缓存及其特性的说法:

  • 在操作系统中我们学过,LRU缓存是一种队列-如果重新访问某个元素,它将进入回收顺序的末尾。
  • 由于该高速缓存的大小有限,因此该队列将具有特定的容量。每当一个新元素被引入时,它就被添加到队列的头部。当驱逐发生时,它发生在队列的尾部。
  • 命中该高速缓存中的数据必须在恒定的时间内完成,这在Queue中是不可能的!所以我们需要使用Java中的hashmap结构。
  • 删除最近最少使用的元素必须在常量时间内完成,这意味着对于Queue的实现,我们将使用双链表。

将键保留在Map上,以便快速访问队列中的数据。

LRU算法非常简单!如果键存在于HashMap中,则为缓存命中;不然的话就没命中。

在缓存未命中时,执行两个步骤:

  1. 在列表前面添加一个新元素。
  2. HashMap中添加一个新条目,并引用列表的头部。

缓存命中后执行两个步骤:

  1. 删除hit元素并将其添加到列表前面。
  2. 使用列表前面的新引用更新HashMap

三、Java的实现

首先定义一个cache接口:

public interface Cache<K, V> {
    boolean set(K key, V value);
    Optional<V> get(K key);
    int size();
    boolean isEmpty();
    void clear();
}

然后是LRUCache类:

public class LRUCache<K, V> implements Cache<K, V> {
    private int size;
    private Map<K, LinkedListNode<CacheElement<K,V>>> linkedListNodeMap;
    private DoublyLinkedList<CacheElement<K,V>> doublyLinkedList;

    public LRUCache(int size) {
        this.size = size;
        this.linkedListNodeMap = new HashMap<>(maxSize);
        this.doublyLinkedList = new DoublyLinkedList<>();
    }
   
}

这里使用HashMap集合来存储对LinkedListNode的所有引用,就是我们上图所见

对应的操作

put操作:
public boolean put(K key, V value) {
    CacheElement<K, V> item = new CacheElement<K, V>(key, value);
    LinkedListNode<CacheElement<K, V>> newNode;
    if (this.linkedListNodeMap.containsKey(key)) {
        LinkedListNode<CacheElement<K, V>> node = this.linkedListNodeMap.get(key);
        newNode = doublyLinkedList.updateAndMoveToFront(node, item);
    } else {
        if (this.size() >= this.size) {
            this.evictElement();
        }
        newNode = this.doublyLinkedList.add(item);
    }
    if(newNode.isEmpty()) {
        return false;
    }
    this.linkedListNodeMap.put(key, newNode);
    return true;
 }
public LinkedListNode<T> updateAndMoveToFront(LinkedListNode<T> node, T newValue) {
    if (node.isEmpty() || (this != (node.getListReference()))) {
        return dummyNode;
    }
    detach(node);
    add(newValue);
    return head;
}

首先,我们在存储所有键/引用的linkedListNodeMap中找到键。如果键存在,则缓存命中发生,它准备从DoublyLinkedList检索CacheElement并将其移动到前面。之后,我们用一个新的引用更新linkedListNodeMap,并将其移动到列表的前面。

我们检查节点是否为空。此外,节点的引用必须与列表相同。然后,我们从列表中分离节点,并将newValue添加到列表中。

若没有键,也就是缓存未命中,我们必须将新键放入linkedListNodeMap。在此之前,我们检查列表大小。如果列表已满,我们必须从列表中删除最近使用最少的元素。

get操作:
public Optional<V> get(K key) {
   LinkedListNode<CacheElement<K, V>> linkedListNode = this.linkedListNodeMap.get(key);
   if(linkedListNode != null && !linkedListNode.isEmpty()) {
       linkedListNodeMap.put(key, this.doublyLinkedList.moveToFront(linkedListNode));
       return Optional.of(linkedListNode.getElement().getValue());
   }
   return Optional.empty();
 }

public LinkedListNode<T> moveToFront(LinkedListNode<T> node) {
    return node.isEmpty() ? dummyNode : updateAndMoveToFront(node, node.getElement());
}

首先,我们从linkedListNodeMap获取节点,然后检查它是否为null或空。其余的操作与前面的操作相同,只有moveToFront方法上的一个区别:

以上内容看懂了可以进行一个小实战:

@Test
public void addDataToCacheToTheNumberOfSize_WhenAddOneMoreData_ThenLeastRecentlyDataWillEvict(){
    LRUCache<String,String> lruCache = new LRUCache<>(3);
    lruCache.put("1","test1");
    lruCache.put("2","test2");
    lruCache.put("3","test3");
    lruCache.put("4","test4");
    assertFalse(lruCache.get("1").isPresent());
 }

到目前为止都是单线程的环境,为了使这个容器是线程安全的,我们需要同步所有的公共方法。

可以使用可重入的读、写锁,因为它使我们在阅读和写时使用锁时具有更大的灵活性。

之后的并发以后再说,上述内容可以满足面试手撕需要了。

  • 8
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java实现LRU缓存淘汰算法的方法与Python类似,也可以使用哈希表和双向链表来实现。下面是一个Java实现LRU缓存淘汰算法的代码示例: ```java class LRUCache { private Map<Integer, Node> map; private int capacity; private Node head; private Node tail; public LRUCache(int capacity) { this.capacity = capacity; map = new HashMap<>(); head = new Node(0, 0); tail = new Node(0, 0); head.next = tail; tail.prev = head; } public int get(int key) { if (map.containsKey(key)) { Node node = map.get(key); remove(node); add(node); return node.value; } else { return -1; } } public void put(int key, int value) { if (map.containsKey(key)) { Node node = map.get(key); node.value = value; remove(node); add(node); } else { if (map.size() == capacity) { Node node = tail.prev; remove(node); map.remove(node.key); } Node node = new Node(key, value); map.put(key, node); add(node); } } private void add(Node node) { Node next = head.next; head.next = node; node.prev = head; node.next = next; next.prev = node; } private void remove(Node node) { Node prev = node.prev; Node next = node.next; prev.next = next; next.prev = prev; } private class Node { int key; int value; Node prev; Node next; public Node(int key, int value) { this.key = key; this.value = value; } } } ``` 在这个实现,我们同样使用了一个哈希表来查询节点是否存在以及快速删除节点,使用一个双向链表来维护缓存节点的顺序。当有新的节点被访问时,我们将其移到链表头部,并且当缓存空间不足时,我们淘汰链表尾部的节点。同时,我们使用了一个Node内部类来封装节点的key和value,以及前驱和后继节点的指针。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值