如何用链表来实现 LRU 缓存淘汰策略呢?

227 阅读2分钟

原理

1、需要给缓存设置固定大小

2、每次读取缓存都该变缓存的使用时间,

3、在缓存满了以后,需要将最早的缓存数据删除,再添加新的元素

LinkedHashMap实现

这是LinkedHashMap的一个构造函数,传入的第三个参数accessOrder为true的时候,就按访问顺序对LinkedHashMap排序,为false的时候就按插入顺序,默认是为false的。 当把accessOrder设置为true后,就可以将最近访问的元素置于最前面,这样就可以满足上述的第二点。


public class LinkedHashMapLRU<K, V> {

    private final int MAX_CACHE_SIZE;

    private final float DEFAULT_LOAD_FACTORY = 0.75f;

    /**
     * LRU缓存
     */
    private LinkedHashMap<K, V> map;


    public LinkedHashMapLRU(int cacheSize) {
        this.MAX_CACHE_SIZE = cacheSize;
        int capacity = (int) Math.ceil(MAX_CACHE_SIZE / DEFAULT_LOAD_FACTORY) + 1;
        map = new LinkedHashMap<K, V>(capacity, DEFAULT_LOAD_FACTORY, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry eldest) {
                return size() > MAX_CACHE_SIZE;
            }
        };
    }


    public synchronized void put(K key, V value) {
        map.put(key, value);
    }

    public synchronized V get(K key) {
        return map.get(key);
    }

    public synchronized void remove(K key) {
        map.remove(key);
    }

    public synchronized Set<Map.Entry<K, V>> getAll() {
        return map.entrySet();
    }

    @Override
    public String toString() {
        StringBuilder stringBuilder = new StringBuilder();
        for (Map.Entry<K, V> entry : map.entrySet()) {
            stringBuilder.append(String.format("%s: %s  ", entry.getKey(), entry.getValue()));
        }
        return stringBuilder.toString();
    }

    public static void main(String[] args) {
        LinkedHashMapLRU<Integer, Integer> lru1 = new LinkedHashMapLRU<>(5);
        lru1.put(1, 1);
        lru1.put(2, 2);
        lru1.put(3, 3);
        System.out.println(lru1);
        lru1.get(1);
        System.out.println(lru1);
        lru1.put(4, 4);
        lru1.put(5, 5);
        lru1.put(6, 6);
        System.out.println(lru1);
    }
}


运行结果

1: 1  2: 2  3: 3  
2: 2  3: 3  1: 1  
3: 3  1: 1  4: 4  5: 5  6: 6  

用HashMap实现

当数据每一次查询就将数据放到链表的head,当有新数据添加时也放到head上。这样链表的tail就是最久没用使用的缓存数据,每次容量不足的时候就可以删除tail,并将前一个元素设置为tail,显然这是一个双向链表结构,因此我们定义LRUNode如下:

LRUNode.java

class LRUNode<K,V> {
    K key;
    V value;
    LRUNode prev;
    LRUNode next;
    public LRUNode(K key, V value) {
        this.key = key;
        this.value = value;
    }
}

LRUCache.java

public class LRUCache<K, V> {

    /**
     * 缓存
     */
    private HashMap<K, LRUNode<K, V>> map;

    /**
     * 容量
     */
    private int capacity;

    /**
     * 头结点
     */
    private LRUNode head;

    /**
     * 尾结点
     */
    private LRUNode tail;

    /**
     * 设置缓存
     *
     * @param key   键
     * @param value 值
     */
    public void set(K key, V value) {
        LRUNode node = map.get(key);
        // 节点不为空,则更新值,删删除节点
        if (node != null) {
            node.value = value;
            remove(node, false);
        }
        // 节点为空,则创建节点
        else {
            node = new LRUNode(key, value);
            if (map.size() >= capacity) {
                // 每次容量不足时先删除最久未使用的元素
                remove(tail, true);
            }
            map.put(key, node);
        }
        // 将刚添加的元素设置为head
        setHead(node);
    }

    /**
     * 获取指定的键的值
     *
     * @param key 键
     * @return
     */
    public V get(K key) {
        LRUNode<K, V> node = map.get(key);
        if (node != null) {
            // 将刚操作的元素放到head
            remove(node, false);
            setHead(node);
            return node.value;
        }
        return null;
    }

    /**
     * 将元素添加到head
     *
     * @param node
     */
    private void setHead(LRUNode node) {
        // 先从链表中删除该元素
        if (head != null) {
            node.next = head;
            head.prev = node;
        }
        head = node;
        if (tail == null) {
            tail = node;
        }
    }

    /**
     * 从链表中删除此Node,此时要注意该Node是head或者是tail的情形
     *
     * @param node
     * @param flag
     */
    private void remove(LRUNode node, boolean flag) {
        if (node.prev != null) {
            node.prev.next = node.next;
        } else {
            head = node.next;
        }
        if (node.next != null) {
            node.next.prev = node.prev;
        } else {
            tail = node.prev;
        }
        node.next = null;
        node.prev = null;
        if (flag) {
            map.remove(node.key);
        }
    }

    public LRUCache(int capacity) {
        this.capacity = capacity;
        this.map = new HashMap<K, LRUNode<K, V>>();
    }
}