LRU算法的两种简单实现
一、什么是LRU
我们来看一下百度百科的解释。
LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 t,当须淘汰一个页面时,选择现有页面中其 t 值最大的,即最近最少使用的页面予以淘汰。
这里只是说页面置换算法中的一种,其实我们缓存中这种算法也是很常见的,mysql底层其实用的是这种置换算法。一般我们做缓存不希望内存被大量消耗导致OOM的时候,这种算法是很常用的,比如redis默认的就是一种伪lru,原理是一样的。今天就来自己简单实现一下这种算法。
二、两种实现方式
1.第一种利用LinkedHashMap的方式实现
研究过HashMap源码的人都应该会发现不管是putAll方法还是一个get方法,只要访问到元素的方法或多或少都会出现下面几个莫名的方法。
刚开始研究源码的时候觉得这不是有毛病吧,调用几个空方法什么意思。后面看多了就发现这是维护热点数据功能的LinkedHashMap而实现的。如果你想要维持写入HashMap元素的顺序或者维持一个热点的话,可以选择LinkedHashMap快捷实现。接下来我就来利用LinkedHashMap来简单实现一个demo,demo是这样的一个直播间只允许5个人进入,当直播间里面已经有五个人的时候,第6个人要想进入,就必须将最早进入或者最近时间发言最少的人踢出直播间。具体demo如下:
LruCache.java
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 利用LinkedHashMap 自定义实现lru算法
* @param <K>
* @param <V>
*/
public class LruCache<K,V> extends LinkedHashMap {
private int capacity;
public LruCache(int capacity){
super(capacity,0.75F,true);
this.capacity = capacity;
}
// 进入直播间的人发言方法
public V get(Object key){
return (V) super.get(key);
}
// 进入直播间的方法
public V put(Object key, Object value){
V oldValue = (V) super.put(key,value);
return oldValue;
}
// 进来之后最近最久为发言的人被自动踢出
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > capacity;
}
} 利用这个类我们来实现这个demo: PutRoom.java
import java.util.Iterator;
public class PutRoom {
public static void main(String[] args) {
// 验证利用LinkedHashMap
LruCache<Integer,String> lruCache = new LruCache<Integer, String>(5);
for(int i = 1; i < 6; i++){
lruCache.put(i,i+1);
System.out.println("第 " + i +" 个人进入房间");
}
System.out.print("此时在房间中的人:");
Iterator iterator = (Iterator) lruCache.keySet().iterator();
while (iterator.hasNext()){
System.out.print(iterator.next() + " ");
}
lruCache.get(1);
System.out.println();
System.out.println("第1个进入房间的人说话了!");
lruCache.put(6,"第 " + 6 + " 个人进入了房间");
System.out.println("第二个进入房间的人被踢了,应为他比剩下的没发言的都早进来!!");
System.out.print("房间中剩余的人:");
Iterator peopleAfter = (Iterator) lruCache.keySet().iterator();
while (peopleAfter.hasNext()){
System.out.print(peopleAfter.next() + " ");
}
}
} 我们可以来看看结果: 很明显达到到了我们的要求。这里是把热点数据放到了链表尾部,接下来我们自己实现一个把热点数据链表头部的算法吧。
2.第二种我们利用HashTable + 双向链表来实现
这种实现方式其实和上面的原理是一样的,只不过这是我们自己来实现一个双端链表的结构,将热点数据放在链表头部以及插入和删除。这里的话也和上面一样实现一个demo。具体看下面代码:
CustomLruCache.java
import java.util.Hashtable;
public class CustomLruCache {
/**
* 自定义双端链表
*/
class DLinkedNode {
int key;
String value;
DLinkedNode prev;
DLinkedNode next;
}
/**
* 链表操作的新增节点,并使用头插法,保持热点数据在头部
* @param node
*/
private void addNode(DLinkedNode node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
/**
* 链表操作的移除节点
* @param node
*/
private void removeNode(DLinkedNode node) {
DLinkedNode prev = node.prev;
DLinkedNode next = node.next;
prev.next = next;
next.prev = prev;
}
/**
* 将节点移动到头部
* @param node
*/
private void moveToHead(DLinkedNode node) {
removeNode(node);
addNode(node);
}
/**
* 将尾部节点移除
* @return
*/
private DLinkedNode popTail() {
DLinkedNode res = tail.prev;
removeNode(res);
return res;
}
private Hashtable<Integer, DLinkedNode> cache =
new Hashtable<Integer, DLinkedNode>();
private int size;
private int capacity;
private DLinkedNode head, tail;
public CustomLruCache(int capacity) {
this.size = 0;
this.capacity = capacity;
// 表头
head = new DLinkedNode();
// 表尾
tail = new DLinkedNode();
head.next = tail;
tail.prev = head;
}
/**
* 获取节点,不存在返回-1
* 存在移动到链表头部并返回
* @param key
* @return
*/
public String get(int key) {
DLinkedNode node = cache.get(key);
if (node == null) return "-1";
moveToHead(node);
return node.value;
}
/**
* 插入节点,保证热点在头部
* @param key
* @param value
*/
public void put(int key, String value) {
DLinkedNode node = cache.get(key);
// 插入节点不存在
if (node == null) {
DLinkedNode newNode = new DLinkedNode();
newNode.key = key;
newNode.value = value;
cache.put(key, newNode);
addNode(newNode);
++size;
// 节点数大于最大容量,移除非热点数据
if (size > capacity) {
DLinkedNode tail = popTail();
cache.remove(tail.key);
--size;
}
// 插入节点已存在,更新值并移动到链表头部
} else {
node.value = value;
moveToHead(node);
}
}
} 我们简单来测试一下
LruExample.java
public class LruExample {
public static void main(String[] args){
CustomLruCache customLruCache = new CustomLruCache(5);
for(int i = 0; i < 5; i++){
customLruCache.put(i,i+"");
}
System.out.print("容器key = 0的初始内容:");
System.out.println(customLruCache.get(0));
System.out.println("key = 0,被获取了一次,所以会在队头,新增一个,导致key = 1被淘汰");
customLruCache.put(6,"6");
System.out.println("里面没有key = 1的,以及被淘汰,索引返回值为" + customLruCache.get(1));
}
} 输出结果: 符合逾期,nice!!
三、附录
好了我们的两个LRU简单算法就到这里了,案例的源代码在我的github上。 https://github.com/Issocala/java-learner/tree/master/LRU-algorithm
这里有我的一些学习留下的代码,欢迎大家一起来学习。
查看14道真题和解析