LRU含义

LRU全称是Least Recently Used,即最近最久未使用的意思。

LRU算法的设计原则是:如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。

LRU的实现思路

  1. 使用数组存储数据,给每个数据项增加一个长整型标识(初始值可设置为时间戳),每次插入数据时先给已有的数据项的时间戳自增,然后把待插入的时间戳置为0,并插入到数组中;每次访问数组中的数据项的时候,先把被访问的数据项的标识置为0,当数组长度满了的时候把标识最大的删除
  2. 利用链表存储数据,每次插入的时候把数据项插入到链尾,取数据的时候把取得数据移动到链尾,被取数据的原始前后项直接相连,当满了的时候删除链表头部的数据
  3. 利用链表和hashMap,原理和单独的链表一致,只是提高了获取数据的效率

对于第一种方法, 需要不停地维护数据项的访问时间戳,另外,在插入数据、删除数据以及访问数据时,时间复杂度都是O(n)。

对于第二种方法,链表在定位数据的时候时间复杂度为O(n)。

所以在一般使用第三种方式来是实现LRU算法。


缓存淘汰LRU算法_淘汰算法

基于双向链表+HashMap

package cn.medsci.form;

import java.util.HashMap;
import java.util.Map;

/**
* @author : zhenguo.yao 2022-02-11 13:35
*/
public class LruCacheTest2 {

/*
* 删除最近最少访问,需要有以下几个元素
* 最大容量,超出最大容量后触发删除
* 元素最终的访问顺序
* 头尾节点,数据访问后节点顺序需要变更,以适应最终删除最少访问的需求
* 需要有一个快速查询的容器
*/

/**
* 最大容量
*/
private int maxLimit = 5;

/**
* 快速查询的容器
*/
private final Map<String, Node> CACHE;

/**
* 头尾节点
*/
private Node headNode, tailNode;

public LruCacheTest2(int size) {
CACHE = new HashMap<>(size);
maxLimit = size;
}

/**
* 获取数据
*
* @param key
* @return
*/
private String get(String key) {
// 获取数据
Node current = CACHE.get(key);
if (null == current) {
return null;
}
refreshNode(current);
return current.value;
}

/**
* 添加数据
*
* @param key
* @param value
* @return
*/
public void put(String key, String value) {
Node node = CACHE.get(key);
if (null == node) {
// 节点不存在
if (CACHE.size() >= maxLimit) {
// 超出限制了
// 删除头节点
String oldKey = removeNode(headNode);
// 删除map数据
CACHE.remove(oldKey);
}
node = new Node(key, value);
addNode(node);
CACHE.put(key, node);
} else {
// 节点存在,刷新节点
node.value = value;
refreshNode(node);
}
}

/**
* 删除数据
*
* @param key
*/
public void remove(String key) {
Node node = CACHE.get(key);
if (null != node) {
removeNode(node);
CACHE.remove(key);
}
}


/**
* 节点信息
*/
private static class Node {

public Node(String key, String value) {
this.key = key;
this.value = value;
}

private String key;

private String value;

private Node preNode;

private Node nextNode;

}

/**
* 删除指定节点
*
* @param node
* @return
*/
private String removeNode(Node node) {
if (node == tailNode) {
// 删除尾结点,那么尾结点的前一个节点成为新的尾结点
tailNode = tailNode.preNode;
} else if (node == headNode) {
// 删除头节点,那么头结点的下一个节点成为新的头结点
headNode = headNode.nextNode;
} else {
// 删除中间节点
// 当前节点前一个节点的后一个节点 等于 当前节点的后一个节点
node.preNode.nextNode = node.nextNode;
// 当前节点后一个节点的前一个节点 等于 当前节点的前一个节点
node.nextNode.preNode = node.preNode;
}
return node.key;
}

/**
* 增加节点到尾部
*
* @param node
* @return
*/
private void addNode(Node node) {
if (tailNode != null) {
tailNode.nextNode = node;
node.preNode = tailNode;
node.nextNode = null;
}
tailNode = node;
if (headNode == null) {
headNode = node;
}
}

/**
* 刷新节点(刷新被访问的节点位置)
*
* @param node
*/
private void refreshNode(Node node) {
if (node == tailNode) {
// 访问的就是尾结点,不需要刷新
return;
}
// 删除节点
removeNode(node);
// 重新插入节点
addNode(node);
}

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

lruCache.put("001", "用户1信息");

lruCache.put("002", "用户1信息");

lruCache.put("003", "用户1信息");

lruCache.put("004", "用户1信息");

lruCache.put("005", "用户1信息");

lruCache.get("002");

lruCache.put("004", "用户2信息更新");

lruCache.put("006", "用户6信息");

System.out.println(lruCache.get("001"));

System.out.println(lruCache.get("006"));
}
}

其实JDK中已经帮我们实现了hash+链表的数据结构,LinkedHashMap,我们查看源码时发现默认的linkedHashMap保证了迭代的顺序,该迭代顺序可以是插入顺序(默认),也可以是访问顺序;

/**
* 构造一个具有指定初始容量、加载因子和排序模式的空LinkedHashMap实例。
*
* 参数:
* initialCapacity - 初始容量
* loadFactor – 负载因子
* accessOrder – 排序模式 - 访问顺序为true ,插入顺序为false
* 抛出:
* IllegalArgumentException – 如果初始容量为负或负载因子为非正
* 推断注释:
* @org.jetbrains.annotations.Contract(pure = true)
*/
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}

此时我们设置accessOrder=true就可以保证按照访问顺序保证迭代顺序,那么顺序保证了怎么限定最大长度呢?因为我们知道默认情况下Map是会扩容的,不用担心,linkedHashMap有相关实现,show me the code~~

/**
* 如果此映射应删除其最旧的条目,则返回true 。在将新条目插入映射后, put和putAll调用此方法。它为实现者提供了在每次添加新条目时删除最旧条目的机会。如果映射表示缓存,这很有用:它允许映射通过删除过时的条目来减少内存消耗。
* 示例使用:此覆盖将允许映射增长到 100 个条目,然后在每次添加新条目时删除最旧的条目,保持 100 个条目的稳定状态。
* private static final int MAX_ENTRIES = 100;
*
* protected boolean removeEldestEntry(Map.Entry eldest) {
* return size() > MAX_ENTRIES;
* }
*
* 此方法通常不会以任何方式修改映射,而是允许映射按照其返回值的指示修改自身。此方法允许直接修改地图,但如果这样做,它必须返回false (表示地图不应尝试任何进一步的修改)。未指定在此方法中修改地图后返回true的效果。
* 此实现仅返回false (因此此贴图的作用类似于法线贴图 - 永远不会删除最老的元素)。
*
* 参数:
* eldest – 映射中最近最少插入的条目,或者如果这是按访问排序的映射,则为最近最少访问的条目。这是将被删除的条目,此方法返回true 。如果在put或putAll调用导致此调用之前映射为空,则这将是刚刚插入的条目;换句话说,如果映射包含单个条目,则最旧的条目也是最新的。
* 回报:
* 如果应该从地图中删除最旧的条目,则为true ;如果应该保留它,则为false 。
*/
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}

也就是说我们只需要把该方法重写成我们想要的实现就行了;看吧,就是这么简单,原理都知道了,还等什么,展示~~

基于LinkedHashMap

package cn.medsci.form;

import java.util.LinkedHashMap;
import java.util.Map;

/**
* 最近最少使用缓存清除策略
*
* @author : zhenguo.yao 2022-02-11 10:23
*/
public class LruCacheTest {

private static final Map<String, String> CACHE = new FixLinkedHashMap<>(5);

public static void main(String[] args) {
CACHE.put("key1", "value1");
CACHE.put("key2", "value2");
CACHE.put("key3", "value3");
CACHE.put("key4", "value4");
CACHE.put("key5", "value5");
CACHE.put("key6", "value6");

CACHE.forEach((k, v) -> System.out.println("k=" + k + ",v=" + v));

CACHE.get("key4");

System.out.println("------------------------------");
CACHE.forEach((k, v) -> System.out.println("k=" + k + ",v=" + v));

}


private static class FixLinkedHashMap<K, V> extends LinkedHashMap<K, V> {

private final int capacity;

public FixLinkedHashMap(int capacity) {
super(capacity, 0.75f, true);
this.capacity = capacity;
}

@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
}


}

打完收工,如有疑问/建议欢迎评论区批评指正~~