LRU算法(2种实现)

目录

LRU原理

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

实现1

最常见的实现是使用一个链表保存缓存数据,详细算法实现如下: 

1. 新数据插入到链表头部; 
2. 每当缓存命中(即缓存数据被访问),则将数据移到链表头部; 
3. 当链表满的时候,将链表尾部的数据丢弃。 
分析 
【命中率】 
当存在热点数据时,LRU的效率很好,但偶发性的、周期性的批量操作会导致LRU命中率急剧下降,缓存污染情况比较严重。 
【复杂度】 
实现简单。 
【代价】 
命中时需要遍历链表,找到命中的数据块索引,然后需要将数据移到头部。

package ms.cache1;

import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author: weicl
 * @Date: 2019/12/19 4:31 PM
 * @Version 1.0
 * @Description 类说明:利用LinkedHashMap实现简单的缓存, 必须实现removeEldestEntry方法,当返回true的时,会删除链表尾部的元素。具体参见JDK文档
 */
@Slf4j
public class LRULinkedHashMap<K, V> extends LinkedHashMap<K, V> {

    private final int maxCapacity;

    private static final float DEFAULT_LOAD_FACTOR = 0.75f;

    private final Lock lock = new ReentrantLock();

    public LRULinkedHashMap(int maxCapacity) {
        super(maxCapacity, DEFAULT_LOAD_FACTOR, true);
        this.maxCapacity = maxCapacity;
    }

    @Override
    protected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest) {
        return size() > maxCapacity;
    }

    @Override
    public boolean containsKey(Object key) {
        try {
            lock.lock();
            return super.containsKey(key);
        } finally {
            lock.unlock();
        }
    }


    @Override
    public V get(Object key) {
        try {
            lock.lock();
            return super.get(key);
        } finally {
            lock.unlock();
        }
    }

    @Override
    public V put(K key, V value) {
        try {
            lock.lock();
            return super.put(key, value);
        } finally {
            lock.unlock();
        }
    }

    @Override
    public int size() {
        try {
            lock.lock();
            return super.size();
        } finally {
            lock.unlock();
        }
    }

    @Override
    public void clear() {
        try {
            lock.lock();
            super.clear();
        } finally {
            lock.unlock();
        }
    }

    public Collection<Map.Entry<K, V>> getAll() {
        try {
            lock.lock();
            return new ArrayList<Map.Entry<K, V>>(super.entrySet());
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        LRULinkedHashMap lruLinkedHashMap = new LRULinkedHashMap(10);
        for (int i = 1; i <= 10; i++) {
            lruLinkedHashMap.put("k" + i, i);
        }
        log.info(lruLinkedHashMap.toString());
        lruLinkedHashMap.get("k3");
        log.info(lruLinkedHashMap.toString());
        lruLinkedHashMap.put("k11", 11);
        log.info(lruLinkedHashMap.toString());
    }

}

测试类:

  • package ms.cache1;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.LinkedHashMap;
    
    /**
     * @Author: weicl
     * @Date: 2019/12/19 4:35 PM
     * @Version 1.0
     * @Description ${description}
     */
    @Slf4j
    public class LRULinkedHashMapTest {
    
        public static void main(String[] args) {
            LRULinkedHashMap lruLinkedHashMap = new LRULinkedHashMap(10);
            for (int i = 1; i <= 10; i++) {
                lruLinkedHashMap.put("k" + i, i);
            }
            log.info(lruLinkedHashMap.toString());
            lruLinkedHashMap.get("k3");
            log.info(lruLinkedHashMap.toString());
            lruLinkedHashMap.put("k11", 11);
            log.info(lruLinkedHashMap.toString());
        }
    
    
    }
    
    测试结果:
  • {k1=1, k2=2, k3=3, k4=4, k5=5, k6=6, k7=7, k8=8, k9=9, k10=10}
    {k1=1, k2=2, k4=4, k5=5, k6=6, k7=7, k8=8, k9=9, k10=10, k3=3}
    {k2=2, k4=4, k5=5, k6=6, k7=7, k8=8, k9=9, k10=10, k3=3, k11=11}

    实现2

  • LRUCache的链表+HashMap实现
  • 传统意义的LRU算法是为每一个Cache对象设置一个计数器,每次Cache命中则给计数器+1,而Cache用完,需要淘汰旧内容,放置新内容时,就查看所有的计数器,并将最少使用的内容替换掉。

    它的弊端很明显,如果Cache的数量少,问题不会很大, 但是如果Cache的空间过大,达到10W或者100W以上,一旦需要淘汰,则需要遍历所有计算器,其性能与资源消耗是巨大的。效率也就非常的慢了。 
    它的原理: 将Cache的所有位置都用双连表连接起来,当一个位置被命中之后,就将通过调整链表的指向,将该位置调整到链表头的位置,新加入的Cache直接加到链表头中。 
    这样,在多次进行Cache操作后,最近被命中的,就会被向链表头方向移动,而没有命中的,而想链表后面移动,链表尾则表示最近最少使用的Cache。 
    当需要替换内容时候,链表的最后位置就是最少被命中的位置,我们只需要淘汰链表最后的部分即可。 
    上面说了这么多的理论, 下面用代码来实现一个LRU策略的缓存。 
    非线程安全,若实现安全,则在响应的方法加锁。

package ms.cache2;

import java.util.HashMap;

/**
 * @Author: weicl
 * @Date: 2019/12/19 5:07 PM
 * @Version 1.0
 * @Description ${description}
 */
public class LRUCache<K, V> {

    private int currentCacheSize;
    private int cacheCapcity;
    private HashMap<K, CacheNode> caches;
    private CacheNode first;
    private CacheNode last;

    public LRUCache(int size) {
        currentCacheSize = 0;
        this.cacheCapcity = size;
        caches = new HashMap<K, CacheNode>(size);
    }

    public void put(K k, V v) {
        CacheNode node = caches.get(k);
        if (node == null) {
            if (caches.size() >= cacheCapcity) {

                caches.remove(last.key);
                removeLast();
            }
            node = new CacheNode();
            node.key = k;
        }
        node.value = v;
        moveToFirst(node);
        caches.put(k, node);
    }

    public Object get(K k) {
        CacheNode node = caches.get(k);
        if (node == null) {
            return null;
        }
        moveToFirst(node);
        return node.value;
    }

    public Object remove(K k) {
        CacheNode node = caches.get(k);
        if (node != null) {
            if (node.pre != null) {
                node.pre.next = node.next;
            }
            if (node.next != null) {
                node.next.pre = node.pre;
            }
            if (node == first) {
                first = node.next;
            }
            if (node == last) {
                last = node.pre;
            }
        }

        return caches.remove(k);
    }

    public void clear() {
        first = null;
        last = null;
        caches.clear();
    }


    private void moveToFirst(CacheNode node) {
        if (first == node) {
            return;
        }
        if (node.next != null) {
            node.next.pre = node.pre;
        }
        if (node.pre != null) {
            node.pre.next = node.next;
        }
        if (node == last) {
            last = last.pre;
        }
        if (first == null || last == null) {
            first = last = node;
            return;
        }

        node.next = first;
        first.pre = node;
        first = node;
        first.pre = null;

    }

    private void removeLast() {
        if (last != null) {
            last = last.pre;
            if (last == null) {
                first = null;
            } else {
                last.next = null;
            }
        }
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        CacheNode node = first;
        while (node != null) {
            sb.append(String.format("%s:%s ", node.key, node.value));
            node = node.next;
        }

        return sb.toString();
    }

    class CacheNode {
        CacheNode pre;
        CacheNode next;
        Object key;
        Object value;

        public CacheNode() {

        }
    }

    public static void main(String[] args) {

        LRUCache<Integer, String> lru = new LRUCache<Integer, String>(5);

        lru.put(1, "a");    // 1:a
        System.out.println(lru.toString());
        lru.put(2, "b");    // 2:b 1:a
        System.out.println(lru.toString());
        lru.put(3, "c");    // 3:c 2:b 1:a
        System.out.println(lru.toString());
        lru.put(4, "d");    // 4:d 3:c 2:b
        System.out.println(lru.toString());
//        lru.put(1, "aa");   // 1:aa 4:d 3:c
//        System.out.println(lru.toString());
//        lru.put(2, "bb");   // 2:bb 1:aa 4:d
        System.out.println(lru.toString());
        lru.put(5, "e");    // 5:e 2:bb 1:aa
        System.out.println(lru.toString());
//        lru.get(1);         // 1:aa 5:e 2:bb
//        System.out.println(lru.toString());
//        lru.remove(11);     // 1:aa 5:e 2:bb
//        System.out.println(lru.toString());
//        lru.remove(1);      //5:e 2:bb
//        System.out.println(lru.toString());
//        lru.put(1, "aaa");  //1:aaa 5:e 2:bb
        lru.put(6, "f");  //1:aaa 5:e 2:bb
        System.out.println(lru.toString());
    }

}

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值