LRU算法–缓存淘汰算法

目录

1. 什么是 LRU

LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“若是数据最近被访问过,那么未来被访问的概率也很高”,反过来讲“若是数据最近这段时间一直都没有访问,那么未来被访问的几率也会很低”,两种理解是同样的;经常使用于页面置换算法,是为虚拟页式存储管理服务的。java

2. LRU 算法思想

达到这样一种情形的算法是最理想的:每次调换出的页面是全部内存页面中最迟将被使用的;这能够最大限度的推迟页面调换,这种算法,被称为理想页面置换算法。惋惜的是,这种算法是没法实现的。
为了尽可能减小与理想算法的差距,产生了各类精妙的算法,最近最少使用页面置换算法即是其中一个。LRU 算法的提出,是基于这样一个事实:在前面几条指令中使用频繁的页面极可能在后面的几条指令中频繁使用。反过来讲,已经好久没有使用的页面极可能在将来较长的一段时间内不会被用到 。这个,就是著名的局部性原理——比内存速度还要快的cache,也是基于一样的原理运行的。所以,咱们只须要在每次调换时,找到最近最少使用的那个页面调出内存。node

注意:操做系统分页处理使用的分页调度算法是LRU算法的近似实现:Clock算法web

3.为何须要页面置换算法?

  • 内存是有限的,不可能把全部的页面都装进来
    – 缺页时须要进行页面置换算法

  • 页面置换的背后是个通用的问题
    –Web服务器的缓存
    –Redis ,memcached 的缓存
    –……缓存

4.分页原理图

这里写图片描述

5. 假设使用 FIFO (先进先出) 实现页面置换算法

这里写图片描述

很明显, 按照FIFO算法, 虽然一个页面被频繁访问, 它仍是颇有可能被置换出去。服务器

6. LRU 算法原理图

这里写图片描述

7. 使用双向链表实现LRU算法

算法实现:app

public class LRUPageFrame {
    private static class Node{
        Node prev;
        Node next;
        int pageNum;
        Node(){

        }
    }

    private int capacity;//容量

    private int currentSize;
    private Node first;
    private Node last;

    public LRUPageFrame( int capacity ) {
        this.currentSize = 0;
        this.capacity  = capacity;
    }

    /** * 得到缓存中的对象 * @param pageNum */
    public void access( int pageNum ){
        Node node = find( pageNum );
        if( node != null ){
            moveExistingNodeToHead(node);
        } else {

            node = new Node();
            node.pageNum = pageNum;

            // 缓存容器是否已经超过大小.
            if( currentSize >= capacity ){
                removeLast(); 
            }

            addNewNodetoHead(node);
        } 
    }
    private Node find( int data ){
        Node node = first;
        while( node != null ){
            if( node.pageNum == data ){
                return node;
            }
            node = node.next;
        }
        return null;
    }


    /** * 移动到链表头,表示这个节点是最新使用过的 * @param node */
    private void moveExistingNodeToHead( Node node ){
        if( node == first ){
            return;
        } else if( node == last ){
            //当前节点是链表尾, 须要放到链表头
            Node prevNode = node.prev;
            prevNode.next = null;
            last.prev = null;
            last = prevNode;
        } else {
            //node 在链表的中间, 把node 的先后节点链接起来
            Node prevNode = node.prev;
            prevNode.next = node.next;

            Node nextNode = node.next;
            nextNode.prev = prevNode;
        }
        node.prev = null;
        node.next = first;
        first.prev = node;
        first = node;
    }

    /** * 删除链表尾部节点 表示 删除最少使用的缓存对象 */
    private void removeLast(){
        Node prevNode = last.prev;
        prevNode.next = null;
        last.prev = null;
        last = prevNode;
        this.currentSize --;
    }

    private void addNewNodetoHead( Node node ){
        if( isEmpty() ){
            node.prev = null;
            node.next = null;
            first = node;
            last = node;
        } else {
            node.prev = null;
            node.next = first;
            first.prev = node;
            first = node;
        }
        this.currentSize ++;
    }

    private boolean isEmpty(){
        return (first == null) && (last == null);
    }

    public String toString(){
        StringBuilder buffer = new StringBuilder();
        Node node = first;
        while( node != null ){
            buffer.append( node.pageNum );
            node = node.next;
            if( node != null ){
                buffer.append(",");
            }
        }
        return buffer.toString();
    }
}

测试用例:svg

public class LRUPageFrameTest {
    @Test
    public void testAccess() {
        LRUPageFrame frame = new LRUPageFrame(3);
        frame.access(7);
        frame.access(0);
        frame.access(1);
        Assert.assertEquals("1,0,7", frame.toString());
        frame.access(2);
        Assert.assertEquals("2,1,0", frame.toString());
        frame.access(0);
        Assert.assertEquals("0,2,1", frame.toString());
        frame.access(0);
        Assert.assertEquals("0,2,1", frame.toString());
        frame.access(3);
        Assert.assertEquals("3,0,2", frame.toString());
        frame.access(0);
        Assert.assertEquals("0,3,2", frame.toString());
        frame.access(4);
        Assert.assertEquals("4,0,3", frame.toString());
        frame.access(5);
        Assert.assertEquals("5,4,0", frame.toString());
    }   
}

8.Clock算法是公认的很好的近似LRU的算法

只有三个物理页面 逻辑页面的访问次序是:
三、四、二、六、四、3memcached

这里写图片描述

  • 每一个页加一个引用位, 默认值为0,不管读仍是写,都置为1测试

  • 把全部的页组成一个循环队列

  • 选择淘汰页的时候,扫描引用位, 若是是1则改成0(至关于再给该页面一次存活的机会), 并扫描下一个;若是该引用位是0, 则淘汰该页, 换入新的页面

    脚注
    前两节内容节选自—— [ MinHow-我的博客 ]