Least Recently Use


LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

  1. 什么是LRU
  2. LRU的最简单实现
  3. 手写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)