缓存淘汰算法LRU&LFU

LRU(The Least Recently Used,最近最久未使用算法)

介绍

LRU算法的思想是:如果一个数据在最近一段时间没有被访问到,那么可以认为在将来它被访问的可能性也很小。因此,当空间满时,最久没有访问的数据最先被置换(淘汰)

这个缓存算法将最近使用的条目存放到靠近缓存顶部的位置。当一个新条目被访问时,LRU将它放置到缓存的顶部。当缓存达到极限时,较早之前访问的条目将从缓存底部开始被移除。这里会使用到昂贵的算法,而且它需要记录“年龄位”来精确显示条目是何时被访问的。此外,当一个LRU缓存算法删除某个条目后,“年龄位”将随其他条目发生改变。

Java实现

第一种:使用LinkedHashMap
public class LRUCache {

    int capacity;
    Map<Integer,Integer> map;

    public LRUCache(int capacity){
        this.capacity = capacity;
        map = new LinkedHashMap<>();
    }

    public int get(int key){
        //如果没有找到
        if (!map.containsKey(key)){
            return -1;
        }
        //找到了就刷新数据
        Integer value = map.remove(key);
        map.put(key,value);
        return value;
    }

    public void put(int key,int value){
        if (map.containsKey(key)){
            map.remove(key);
            map.put(key,value);
            return;
        }
        map.put(key,value);
        //超出capacity,删除最久没用的即第一个,或者可以复写removeEldestEntry方法
        if (map.size() > capacity){
            map.remove(map.entrySet().iterator().next().getKey());
        }
    }

    public static void main(String[] args) {
        LRUCache lruCache = new LRUCache(10);
        for (int i = 0; i < 10; i++) {
            lruCache.map.put(i,i);
            System.out.println(lruCache.map.size());
        }
        System.out.println(lruCache.map);
        lruCache.put(10,200);
        System.out.println(lruCache.map);
    }
第二种:双链表+hashmap
public class LRUCache {

    private int capacity;
    private Map<Integer,ListNode>map;
    private ListNode head;
    private ListNode tail;

    public LRUCache2(int capacity){
        this.capacity = capacity;
        map = new HashMap<>();
        head = new ListNode(-1,-1);
        tail = new ListNode(-1,-1);
        head.next = tail;
        tail.pre = head;
    }

    public int get(int key){
        if (!map.containsKey(key)){
            return -1;
        }
        ListNode node = map.get(key);
        node.pre.next = node.next;
        node.next.pre = node.pre;
        return node.val;
    }

    public void put(int key,int value){
        if (get(key)!=-1){
            map.get(key).val = value;
            return;
        }
        ListNode node = new ListNode(key,value);
        map.put(key,node);
        moveToTail(node);

        if (map.size() > capacity){
            map.remove(head.next.key);
            head.next = head.next.next;
            head.next.pre = head;
        }
    }

    //把节点移动到尾巴
    private void moveToTail(ListNode node) {
        node.pre = tail.pre;
        tail.pre = node;
        node.pre.next = node;
        node.next = tail;
    }

    //定义双向链表节点
    private class ListNode{
        int key;
        int val;
        ListNode pre;
        ListNode next;

        //初始化双向链表
        public ListNode(int key,int val){
            this.key = key;
            this.val = val;
            pre = null;
            next = null;
        }
    }
}

LFU(Least Frequently Used ,最近最少使用算法)

介绍

顾名思义,LFU算法的思想是:如果一个数据在最近一段时间很少被访问到,那么可以认为在将来它被访问的可能性也很小。因此,当空间满时,最小频率访问的数据最先被淘汰

LFU 算法的描述:

设计一种缓存结构,该结构在构造时确定大小,假设大小为 K,并有两个功能:

  1. set(key,value):将记录(key,value)插入该结构。当缓存满时,将访问频率最低的数据置换掉。
  2. get(key):返回key对应的value值。

算法实现策略:考虑到 LFU 会淘汰访问频率最小的数据,我们需要一种合适的方法按大小顺序维护数据访问的频率。LFU 算法本质上可以看做是一个 top K 问题(K = 1),即选出频率最小的元素,因此我们很容易想到可以用二项堆来选择频率最小的元素,这样的实现比较高效。最终实现策略为小顶堆+哈希表。

Java代码实现

定义Node节点
/** * 节点 */
public static class Node implements Comparable<Node>{ 

	Object key; //键值
	Object value; /** * 访问时间 */
    long time; /** * 访问次数 */
    
    int count; public Node(Object key, Object value, long time, int count) { 		this.key = key; this.value = value; this.time = time; this.count = count;
    } public Object getKey() { return key;
    } public void setKey(Object key) { this.key = key;
    } public Object getValue() { return value;
    } public void setValue(Object value) { this.value = value;
    } public long getTime() { return time;
    } public void setTime(long time) { this.time = time;
    } public int getCount() { return count;
    } public void setCount(int count) { this.count = count;
    }

    @Override public int compareTo(Node o) { int compare = Integer.compare(this.count, o.count); //在数目相同的情况下 比较时间
        if (compare==0){ return Long.compare(this.time,o.time);
        } return compare;
    }
}
定义LFU类
public class LFU<K,V> { /** *  总容量 */
    private int capacity; /** * 所有的node节点 */
    private Map<K, Node> caches; /** * 构造方法
     * @param size */
    public LFU(int size) { this.capacity = size;
       caches = new LinkedHashMap<K,Node>(size);
    }
    
    /** * 添加元素
 	* @param key
 	* @param value 
 	*/
public void put(K key, V value) {
    Node node = caches.get(key); //如果新元素
    if (node == null) { //如果超过元素容纳量
        if (caches.size() >= capacity) { //移除count计数最小的那个key
            K leastKey = removeLeastCount();
            caches.remove(leastKey);
        } //创建新节点
        node = new Node(key,value,System.nanoTime(),1);
        caches.put(key, node);
    }else { //已经存在的元素覆盖旧值
        node.value = value;
        node.setCount(node.getCount()+1);
        node.setTime(System.nanoTime());
    }
    sort();
}
    
    /** * 排序 */
private void sort() {

    List<Map.Entry<K, Node>> list = new ArrayList<>(caches.entrySet());
    Collections.sort(list, new Comparator<Map.Entry<K, Node>>() {
        @Override public int compare(Map.Entry<K, Node> o1, Map.Entry<K, Node> o2) { return o2.getValue().compareTo(o1.getValue());
        }
    });

    caches.clear(); for (Map.Entry<K, Node> kNodeEntry : list) {
        caches.put(kNodeEntry.getKey(),kNodeEntry.getValue());
    }
}

/** * 移除统计数或者时间比较最小的那个
 * @return
 */
private K removeLeastCount() {
    Collection<Node> values = caches.values();
    Node min = Collections.min(values); return (K)min.getKey();

 }
 
 /** * 获取元素
 * @param key
 * @return
 */
public V get(K key){
Node node = caches.get(key); if (node!=null){
    node.setCount(node.getCount()+1);
    node.setTime(System.nanoTime());
    sort(); return (V)node.value;
} return null;
}


public static  void main(String[] args) {

    LFU<Integer, String> lruCache = new LFU<>(5);
    lruCache.put(1, "A");
    lruCache.put(2, "B");
    lruCache.put(3, "C");
    lruCache.put(4, "D");
    lruCache.put(5, "E");
    lruCache.put(6, "F");
    lruCache.get(2);
    lruCache.get(2);
    lruCache.get(3);
    lruCache.get(6);
    //重新put节点3
    lruCache.put(3,"C");

    final Map<Integer, Node> caches = (Map<Integer, Node>) lruCache.caches; 
    for (Map.Entry<Integer, Node> nodeEntry : caches.entrySet()) { 
        System.out.println(nodeEntry.getValue().value); 
    } 
}
    
}

总结

LRU和LFU侧重点不同,LRU主要体现在对元素的使用时间上,而LFU主要体现在对元素的使用频次上。

当存在热点数据时,LRU的效率很好,但偶发性的、周期性的批量操作会导致LRU命中率急剧下降,缓存污染情况比较严重。

LFU的缺陷是:在短期的时间内,对某些缓存的访问频次很高,这些缓存会立刻晋升为热点数据,而保证不会淘汰,这样会驻留在系统内存里面。而实际上,这部分数据只是短暂的高频率访问,之后将会长期不访问,瞬时的高频访问将会造成这部分数据的引用频率加快,而一些新加入的缓存很容易被快速删除,因为它们的引用频率很低。

一般情况下,LFU效率要优于LRU,且能够避免周期性或者偶发性的操作导致缓存命中率下降的问题。但LFU需要记录数据的历史访问记录,一旦数据访问模式改变,LFU需要更长时间来适用新的访问模式,即:LFU存在历史数据影响将来数据的“缓存污染”效用。

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值