Least Recently Use
LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
- 什么是LRU
- LRU的最简单实现
- 手写LRU
- 什么是LRU
- 利用LinkedHashMap实现的简单LRU
- 看看如何使用
- 手写LRU(利用数组)
- 手写LRU(利用LinkedList)
什么是LRU
距离现在最早使用的会被我们替换掉。不够形象的话我们看下面的例子。
插入 | 1 | 2 | 3 | 4 | 2 | 3 | 1 |
位置1 | 1 | 1 | 1 | 2 | 3 | 4 | 2 |
位置2 | null | 2 | 2 | 3 | 4 | 2 | 3 |
位置3 | null | null | 3 | 4 | 2 | 3 | 1 |
…
位置1始终是最早进来的元素,是淘汰位置。新进来的元素如果是新元素直接放在位置3,然后将位置1弹出。如果是已有元素则将其放在位置3并删除之前位置上的已有元素,保持其他元素相对位置不变。
这里的例子就是一个size=3的缓存淘汰实现。
利用LinkedHashMap实现的简单LRU
对于
java.util.LinkedHashMap
我们的认识仅仅只是停留在该map可以按照插入的顺序保存,那是不够的。
linkedHashMap还可以实现按照访问顺序保存元素。
先看看如何利用它实现LRU的吧
public class UseLinkedHashMapCache<K,V> extends LinkedHashMap<K,V>{
private int cacheSize;
public UseLinkedHashMapCache(int cacheSize){
//构造函数一定要放在第一行
super(16,0.75f,true); //那个f如果不加 就是double类型,然后该构造没有该类型的入参。 然后最为关键的就是那个入参 true
this.cacheSize = cacheSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K,V> eldest){ //重写LinkedHashMap原方法
return size()>cacheSize; //临界条件不能有等于,否则会让缓存尺寸小1
}
}
关键点:
- 继承了LinkedHashMap并使用
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
构造函数
- 重写了
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
看看如何使用
public static void main(String[]args){
UseLinkedHashMapCache<Integer,String> cache = new UseLinkedHashMapCache<Integer,String>(4);
cache.put(1, "one");
cache.put(2, "two");
cache.put(3, "three");
cache.put(4, "four");
cache.put(2, "two");
cache.put(3, "three");
Iterator<Map.Entry<Integer,String>> it = cache.entrySet().iterator();
while(it.hasNext()){
Map.Entry<Integer, String> entry = it.next();
Integer key = entry.getKey();
System.out.print("Key:\t"+key);
String Value = entry.getValue(); //这个无需打印...
System.out.println();
}
}
结果是:
Key: 1
Key: 4
Key: 2
Key: 3
与我们表格中的结果一致。
手写LRU(利用数组)
/**
* 用数组写了一个
*
* 有个疑问, 比如当缓存大小为5 这时候1、2、3、4、4 请问最后一个4是应该插入还是不处理呢?
*
* 我个人觉得如果这里理解为缓存的key ,那么就应该是不插入 结果应该还是1、2、3、4、null
* */
public class HandMakeCache {
//添加次数 计数器
static int count =0;
//数组元素 计数器
static int size=0;
//最大长度
int maxSize;
//对象数组
int [] listArray; //为了简略比较
//顺序表的初始化方法
public HandMakeCache(int maxSize)
{
listArray = new int [maxSize];
this.maxSize = maxSize;
}
public int getSize(){
return size;
}
public void insert(int obj) throws Exception {
// 插入过程不应该指定下标,对于用户来讲这应该是透明的,只需要暴露插入的顺序
boolean exist = false; // 每次insert校验一下是否存在
int location = 0; // 对于已有元素,记录其已存在的位置
for (int i = 0; i < maxSize; i++) {
if (obj == listArray[i]) {
exist = true;
location = i; // 记录已存在的位置
}
} // 遍历看是否已有,每次插入都要遍历,感觉性能很差
if (size < this.maxSize) { // 当插入次数小于缓存大小的时候随意插入
if (exist) {
if (location == 0) {
moveArrayElements(listArray,0,size-2);
} else if (location < size - 1) { // 已存在元素不在最新的位置
moveArrayElements(listArray,location,size-2);
}
listArray[size - 1] = obj; // 由于已存在
} else {
listArray[size] = obj;
size++; // 数组未满时才计数
}
} else { // 此时缓存为满,这时候要保留最末端元素先
if (!exist || obj == listArray[0]) { // 新元素添加进来,和最远元素添加进来效果一样
moveArrayElements(listArray,0,maxSize-2);
} else if (obj != listArray[maxSize - 1]) {
moveArrayElements(listArray,location,maxSize-2);
} // 如果添加的是上次添加的元素,则不管了。。
listArray[maxSize - 1] = obj;
}
count++; // 计数
}
public Object get(int index) throws Exception {
return listArray[index];
}
/**
* 平移数组的方法,start是要移动至的头位置,end为最后被移动的位置。
* */
public void moveArrayElements(int [] arr, int start, int end){
for(int i=start;i<=end;i++){
arr[i] = arr[i+1];
}
}
public static void main(String[] args) {
int cacheSize = 5;
HandMakeCache list = new HandMakeCache(cacheSize);
try
{
list.insert(1);
list.insert(2);
list.insert(3);
list.insert(1);
list.insert(3);
list.insert(4);
list.insert(4);
list.insert(5);
// list.insert(3);
for(int i=0;i<cacheSize;i++)
{
System.out.println(list.get(i));
}
System.out.println("成功插入"+count+"次元素.");
}
catch(Exception ex)
{
ex.printStackTrace();
}
}
}
非常重要的一点~ 写LRU之前你一定要知道LRU的正确的含义。。
这里分为几种情况吧..
1. 当数组未满的情况下,随便插
2. 数组满了之后,插入介于头和尾的元素,需要记录其之前存在的下标,然后将大于该下标的元素整体前移。
3. 数组满了之后,插入最新的元素等于什么操作也没有。保持原样
3. 数组满了之后,插入一个不存在的元素 等同于 插入数组最开始的元素。
比如 1、2、3、4 之后插入5 和 1、2、3、4 之后插入1 结果分别为 2、3、4、5和 2、3、4、1。
缺点:
如果利用数组来存储的话,当我们缓存的大小非常大的时候。比如10W,那么假设我们需要淘汰最远的元素,就需要将99999个元素整体往前移一位,这样还仅仅只是替换一次。大量这样的操作是非常低效的,所以我们还是考虑用链表来实现↓。
手写LRU(利用LinkedList)