什么是LRU?
如果你学过操作系统,必然有听过 LRU 算法,他是一种常用的页面置换算法
实现的思想:选择最近最久未使用的数据予以淘汰
LRU算法实现分析
1、底层数据结构的分析
缓存,必须要读 + 写 两个操作 ,如果读的效率要快,底层是采用哈希表结构,写的效率要快,底层就要采用 链表,所以 LRU的底层实现
采用的数据结构就是哈希链表 (哈希表 + 双向链表)
实现LRU算法的两种方式
一、借助Jdk 提供的LinkedHashMap
继承LinkedHashMap,重写里面的淘汰判断removeEldestEntry,
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 借助jdk实现LinkedHashMap实现LRU算法
*/
public class JdkLRUCache extends LinkedHashMap {
//定义缓存的大小
private int cacheSize;
public JdkLRUCache(int cacheSize){
//第一个参数cacheSize 定义LinkedHashMap的初始大小
//0.75是map的加载因子
//第三个参数 是选择策略;true,在缓存中元素被调用到会往前移动
super(cacheSize,0.75F,true);
this.cacheSize = cacheSize;
}
/**
* 重写removeEldestEntry,实现缓存淘汰判断
* @param eldest
* @return
*/
@Override
protected boolean removeEldestEntry (Map.Entry eldest) {
//判断是否超过了超过了缓存的大小
return super.size() > cacheSize;
}
}
写下测试,看下结果
@Test
public void testJdkLRUCache(){
JdkLRUCache jdkLRUCache = new JdkLRUCache(3);
jdkLRUCache.put(1,"a");
jdkLRUCache.put(2,"b");
jdkLRUCache.put(3,"c");
System.out.println(jdkLRUCache.keySet());
jdkLRUCache.put(4,"d");
System.out.println(jdkLRUCache.keySet());
jdkLRUCache.put(3,"c");
System.out.println(jdkLRUCache.keySet());
jdkLRUCache.put(3,"c");
System.out.println(jdkLRUCache.keySet());
jdkLRUCache.put(3,"c");
System.out.println(jdkLRUCache.keySet());
jdkLRUCache.put(5,"x");
System.out.println(jdkLRUCache.keySet());
}
不借助Jdk,手写LRU
第一个,我们的底层实现的数据结构是哈希表 + 双向链表,就是成员变量是
//自定义的双向链表
private DoubleLinkedList doubleLinkedList;
//map实现精确查找
private Map< K, Node< K, V > > map;
第二个,我们要定义缓存的大小,所以又需要一个成员来记录
//定义缓存的大小
private int cacheSize;
双向链表中,组装的是封装了数据的结点,所以我们需要编写一个结点类Node,因为要用map 要查找 Node ,所以Node 结点中需要记录map中定位他的索引值 K key ,还有封装数据的值 V value ,由于是双向链表,所以需要前后各链接一个Node结点
/**
* 封装数据的Node结点
*/
class Node< K, V > {
//前一个结点
private Node pre;
//后一个结点
private Node next;
//封装的数据值
private V value;
//map中保存该结点的索引key
private K key;
//有参构造
public Node (K key, V value) {
this.key = key;
this.value = value;
}
//无参构造
public Node () {
}
}
双向链表类
/**
* 双向链表
*/
class DoubleLinkedList< K, V > {
//头结点
private Node< K, V > head;
//尾结点
private Node< K, V > tail;
//初始化
public DoubleLinkedList () {
head = new Node();
tail = new Node();
//形成闭环
head.next = tail;
tail.next = head;
}
/**
* 添加Node结点到队头
*/
public void addHead (Node< K, V > node) {
//获取到原本队头的结点
Node oldFirstNode = head.next;
//将node挂到head后面
head.next = node;
node.pre = head;
//将node挂到原本队头结点前
node.next = oldFirstNode;
oldFirstNode.pre = node;
}
/**
* 删除掉某个结点
*/
public void removeNode (Node< K, V > node) {
Node pre = node.pre;
Node next = node.next;
pre.next = next;
next.pre = pre;
node.pre = null;
node.next = null;
}
/**
* 获得队列尾部的结点
*/
public Node< K, V > getLastNode () {
return tail.pre;
}
}
下一步,我们初始化 缓存类
public LRUCache (Integer cacheSize) {
this.cacheSize = cacheSize;
map = new HashMap<>();
doubleLinkedList = new DoubleLinkedList();
}
对外提供get,和put方法
/**
* 最新查找
*/
public V get (Integer key) {
Node< K, V > node = map.get(key);
//将node 移动到最前
doubleLinkedList.removeNode(node);
doubleLinkedList.addHead(node);
return node.value;
}
/**
* 添加结点
*/
public void put (K key, V value) {
//将数据封装到Node结点中
Node dataNode = new Node(key, value);
if (map.containsKey(key)) {
Node< K, V > getNode = map.get(key);
//将node 移动到最前
doubleLinkedList.removeNode(getNode);
doubleLinkedList.addHead(getNode);
} else {
//判断缓存是否已经满了
if (map.size() >= cacheSize) {
//移除掉队尾的元素
Node lastNode = doubleLinkedList.getLastNode();
doubleLinkedList.removeNode(lastNode);
//删除掉map中的管理
map.remove(lastNode.key);
}
//将dataNode添加到双向队列中
doubleLinkedList.addHead(dataNode);
//存放到map中进行管理
map.put(key, dataNode);
}
}
为了方便测试查看底层双向链表,重写toString方法
/**
* 重写toString 方便待会测试查看底层双向链表
* @return
*/
@Override
public String toString () {
StringBuilder stringBuilder = new StringBuilder();
Node currentNode = doubleLinkedList.head.next;
for (int i = 0; i < map.size(); i++) {
stringBuilder.append(currentNode.key + " ");
currentNode = currentNode.next;
}
return stringBuilder.toString();
}
所以整个代码的实现是这样子的
package com.demo.LRU;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 手写一个LRU算法
* LRU底层采用的数据结构是双向链表 + map
*/
public class LRUCache< K, V > {
//自定义的双向链表
private DoubleLinkedList doubleLinkedList;
//map实现精确查找
private Map< K, Node< K, V > > map;
//定义缓存的大小
private int cacheSize;
/**
* 封装数据的Node结点
*/
class Node< K, V > {
//前一个结点
private Node pre;
//后一个结点
private Node next;
//封装的数据值
private V value;
//map中保存该结点的索引key
private K key;
//有参构造
public Node (K key, V value) {
this.key = key;
this.value = value;
}
//无参构造
public Node () {
}
}
/**
* 双向链表
*/
class DoubleLinkedList< K, V > {
//头结点
private Node< K, V > head;
//尾结点
private Node< K, V > tail;
//初始化
public DoubleLinkedList () {
head = new Node();
tail = new Node();
//形成闭环
head.next = tail;
tail.next = head;
}
/**
* 添加Node结点到队头
*/
public void addHead (Node< K, V > node) {
//获取到原本队头的结点
Node oldFirstNode = head.next;
//将node挂到head后面
head.next = node;
node.pre = head;
//将node挂到原本队头结点前
node.next = oldFirstNode;
oldFirstNode.pre = node;
}
/**
* 删除掉某个结点
*/
public void removeNode (Node< K, V > node) {
Node pre = node.pre;
Node next = node.next;
pre.next = next;
next.pre = pre;
node.pre = null;
node.next = null;
}
/**
* 获得队列尾部的结点
*/
public Node< K, V > getLastNode () {
return tail.pre;
}
}
public LRUCache (Integer cacheSize) {
this.cacheSize = cacheSize;
map = new HashMap<>();
doubleLinkedList = new DoubleLinkedList();
}
/**
* 最新查找
*/
public V get (Integer key) {
Node< K, V > node = map.get(key);
//将node 移动到最前
doubleLinkedList.removeNode(node);
doubleLinkedList.addHead(node);
return node.value;
}
/**
* 添加结点
*/
public void put (K key, V value) {
//将数据封装到Node结点中
Node dataNode = new Node(key, value);
if (map.containsKey(key)) {
Node< K, V > getNode = map.get(key);
//将node 移动到最前
doubleLinkedList.removeNode(getNode);
doubleLinkedList.addHead(getNode);
} else {
//判断缓存是否已经满了
if (map.size() >= cacheSize) {
//移除掉队尾的元素
Node lastNode = doubleLinkedList.getLastNode();
doubleLinkedList.removeNode(lastNode);
//删除掉map中的管理
map.remove(lastNode.key);
}
//将dataNode添加到双向队列中
doubleLinkedList.addHead(dataNode);
//存放到map中进行管理
map.put(key, dataNode);
}
}
/**
* 重写toString 方便待会测试查看底层双向链表
* @return
*/
@Override
public String toString () {
StringBuilder stringBuilder = new StringBuilder();
Node currentNode = doubleLinkedList.head.next;
for (int i = 0; i < map.size(); i++) {
stringBuilder.append(currentNode.key + " ");
currentNode = currentNode.next;
}
return stringBuilder.toString();
}
}
编写测试
debug看下底层双向链表的变化
@Test
public void testLRUCache(){
LRUCache lruCache = new LRUCache(3);
lruCache.put(1,"a");
lruCache.put(2,"b");
lruCache.put(3,"c");
System.out.println(lruCache.toString());
lruCache.put(4,"d");
System.out.println(lruCache.toString());
lruCache.put(3,"c");
System.out.println(lruCache.toString());
lruCache.put(3,"c");
System.out.println(lruCache.toString());
lruCache.put(3,"c");
System.out.println(lruCache.toString());
lruCache.put(5,"x");
System.out.println(lruCache.toString());
}