Java实现支持LFU/LRU淘汰策略的缓存

48 阅读2分钟

LRU缓存实现思路:用LinkedHashMap做存储结构,因为它内部维护了一个双向链表来记录插入顺序。


public class LRUCache {
    private int capacity;
    private LRUMap cache;
  
    public LRUCache(int capacity) {
        this.capacity = capacity;
        this.cache = new LRUMap(capacity);

    }
    //通过创建一个匿名内部类的方式覆盖了removeEldestEntry方法
    private class LRUMap extends LinkedHashMap<Integer, Integer> {
        public LRUMap(int initialCapacity) {
            super(initialCapacity, 0.75f, true);
        }
      //removeEldestEntry,默认实现返回false
        @Override
        protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
            return size() > LRUCache.this.capacity;
        }
    }
   

    public int get(int key) {
        return cache.getOrDefault(key, -1);
    }

    public void put(int key, int value) {
        cache.put(key, value);
    }
}

LRU的缓存实现很简单,LFU比较复杂。LFU策略中,当缓存满时,先淘汰访问频率最低的数据。如果有多个数据项的访问频率相同,就先淘汰最早被访问的数据。

public class LFUCache {
    HashMap<Integer, Integer> entryMap; //存储键
    HashMap<Integer, Integer> countMap;  //存储每个key及其访问次数
    HashMap<Integer, LinkedHashSet<Integer>> lists; //存储每个使用频率对应的键的集合。
    int capacity;
    int min = -1;
    
    public LFUCache(int capacity) {
        this.capacity = capacity;
        entryMap = new HashMap<>();
        countMap = new HashMap<>();
        lists = new HashMap<>();
        lists.put(1, new LinkedHashSet<>());
    }
    //访问键,如果键存在,将新访问频率记录在countMap里,并从lists中移除老访问频率
    public int get(int key) {
        if (!entryMap.containsKey(key)) return -1;
        int count = countMap.get(key);
        countMap.put(key, count + 1);
        lists.get(count).remove(key);
        if (count == min && lists.get(count).size() == 0) {
            min++;
        }
        if (!lists.containsKey(count + 1)) {
            lists.put(count + 1, new LinkedHashSet<>());
        }
        lists.get(count + 1).add(key);
        return entryMap.get(key);
    }
    //更新键,存在就直接更新值及键的使用频率,如果键不存在,添加新键到缓存中。
    public void put(int key, int value) {
        if (capacity <= 0) return;
        if (entryMap.containsKey(key)) { //如果键存在,更新键的值,并调用get()更新键使用频率
            entryMap.put(key, value);
            get(key);
            return;
        }
        //添加新键前,先检查缓存是否容量超限,超限要删除使用频率最低且最早添加的键。
        if (entryMap.size() >= capacity) {
            int evict = lists.get(min).iterator().next();
            lists.get(min).remove(evict);
            entryMap.remove(evict);
        }
        //将新键添加到entryMap和countMap中,并将其添加到使用频率为1的list中。
        entryMap.put(key, value);
        countMap.put(key, 1);
        min = 1;
        lists.get(1).add(key);
    }

}

上述代码中的存储结构用HashMap线程不安全,改成ConcurrentHashMap确保更新entryMap、countMap和lists操作的原子性。 例:线程A在put操作,线程B在get操作。如果A在更新countMap和lists时被B中断,B看到的entryMap被更新,但counts和lists还未被更新。