【问题标题】:LFU cache - Random/Inconsistent resultsLFU 缓存 - 随机/不一致的结果
【发布时间】:2021-09-19 11:58:58
【问题描述】:

我有以下 LFU 的实现 - 最不常用的 - 缓存。如果具有相同使用计数的元素之间存在平局,则时间戳将用作平局断路器以逐出最近最少使用的元素

public class LFUCache {

    class CacheKey {
        private int counter;
        private long timestamp;
        private int value;

        public CacheKey(int counter, long timestamp, int value) {
            this.counter = counter;
            this.timestamp = timestamp;
            this.value = value;
        }

        long getTimestamp() {
            return this.timestamp;
        }

        int getCounter() {
            return this.counter;
        }

        void incrementCounter() {
            this.counter++;
        }

        void setTimestamp(long timestamp) {
            this.timestamp = timestamp;
        }

        void setValue(int value) {
            this.value = value;
        }

        public boolean equals(Object o) {
            CacheKey other = (CacheKey) o;
            return other.counter == this.counter && other.timestamp == this.timestamp && this.value == other.value;
        }

        public int hashCode() {
            return Objects.hash(this.counter, this.timestamp, this.value);
        }

        @Override
        public String toString() {
            return "CacheKey{" +
                    "counter=" + counter +
                    ", timestamp=" + timestamp +
                    ", value=" + value +
                    '}';
        }
    }

    /**
     * Map from cache key to its corresponding key
     */
    TreeMap<CacheKey, Integer> timeKeeper;

    /**
     * Map from keys to the keys of th timekeeper map
     */
    Map<Integer, CacheKey> cache;

    /**
     * Total capacity of the cache
     */
    int capacity;

    /**
     * Compare first on the least frequently used, so we are keeping the elements of the cache with the smallest
     * counters in the first entries of the tree map.
     * <p>
     * In case of a tie we have to evict the least recently used, so we are using the timestamp entry, and we are keeping
     * the ones with the smaller
     */
    public LFUCache(int capacity) {

        this.timeKeeper = new TreeMap(Comparator.comparingInt(CacheKey::getCounter).thenComparingLong(CacheKey::getTimestamp));
        this.cache = new HashMap();
        this.capacity = capacity;
    }

    public int get(int key) {
        if (this.cache.containsKey(key)) {
            CacheKey searchKey = this.cache.get(key);

            // We have to remove the old entry from the timekeeper map and re add it to the tree map
            // since if we update only the reference the sorting will not take place again
            this.timeKeeper.remove(searchKey);

            // Update the entry in the timekeeper map with an updated counter and a new last used timestamp
            searchKey.incrementCounter();
            searchKey.setTimestamp(System.currentTimeMillis());
            this.timeKeeper.put(searchKey, key);
            return this.cache.get(key).value;
        } else return -1;
    }

    public void put(int key, int value) {
        // Check if we have to remove something from the map
        if (this.cache.size() == this.capacity) {

            // Remove the least frequently used OR - in case of a tie - least recently used element
            Map.Entry<CacheKey, Integer> lfuEntry = this.timeKeeper.pollFirstEntry();
            CacheKey lfuKey = lfuEntry.getKey();
            Integer lfuValue = lfuEntry.getValue();

            // Remove elements from both cache and timekeeper
            this.timeKeeper.remove(lfuKey);
            this.cache.remove(lfuValue);
        }

        // Check if it is present
        if (this.cache.containsKey(key)) {
            CacheKey searchKey = this.cache.get(key);

            // Remove the old entry from the timekeeper map. Again here we remove the old entry from the timekeeper map
            // and re add it to the tree map since if we update only the reference the sorting will not take place
            this.timeKeeper.remove(searchKey);

            // Update the entry in the timekeeper map with an updated counter and a new last used timestamp
            searchKey.incrementCounter();
            searchKey.setTimestamp(System.currentTimeMillis());
            searchKey.setValue(value);

            this.timeKeeper.put(searchKey, key);
            this.cache.put(key, searchKey);

        } else {
            // Now add the new element
            doPut(key, value);
        }
    }

    private void doPut(int key, int value) {
        CacheKey cacheKey = new CacheKey(1, System.currentTimeMillis(), value);
        this.timeKeeper.put(cacheKey, key);
        this.cache.put(key, cacheKey);
    }
}

我有以下测试来验证一切是否按预期工作

    public static void main(String[] args) {
            LFUCache lfuCache = new LFUCache(2);
            lfuCache.put(1, 1);
            lfuCache.put(2, 2);
            System.out.println(lfuCache.get(1));
            lfuCache.put(3, 3);
            System.out.println(lfuCache.get(2));
            System.out.println(lfuCache.get(3));
            lfuCache.put(4, 4);
            System.out.println(lfuCache.get(1));
            System.out.println(lfuCache.get(3));
            System.out.println(lfuCache.get(4));

    }

我得到的结果是随机的。我希望得到

1
-1
3
-1
3
4

我有时会得到这个,但我也会得到

1
2
3
-1
3
4

1
-1
3
-1
3
4

以及各种其他结果。我尝试创建同步方法

public synchronized int get(int key) {...}
public synchronized void put(int key, int value) {...}

我仍然得到随机结果 我尝试在方法中同步块,例如

    public int get(int key) {
        if (this.cache.containsKey(key)) {
            synchronized(this){
                CacheKey searchKey = this.cache.get(key);

                // Remove the old entry from the timekeeper map
                this.timeKeeper.remove(searchKey);

                // Create a new entry in the timekeeper map with an updated counter and a new last used timestamp
                CacheKey updatedKey = new CacheKey(searchKey.getCounter() + 1, System.currentTimeMillis(), searchKey.value);
                this.timeKeeper.put(updatedKey, key);
                return this.cache.get(key).value;
            }
            
        } else return -1;
    }

我仍然得到随机结果

我尝试在 timeKeepercache 对象上进行同步,例如

    public int get(int key) {
        if (this.cache.containsKey(key)) {
            synchronized(this.timeKeeper){
                synchronized(this.cache){
                    CacheKey searchKey = this.cache.get(key);

                    // Remove the old entry from the timekeeper map
                    this.timeKeeper.remove(searchKey);

                    // Create a new entry in the timekeeper map with an updated counter and a new last used timestamp
                    CacheKey updatedKey = new CacheKey(searchKey.getCounter() + 1, System.currentTimeMillis(), searchKey.value);
                    this.timeKeeper.put(updatedKey, key);
                    return this.cache.get(key).value;
                }
            }
        } else return -1;
    }

但我仍然得到随机结果

我也尝试使用 SyncronizedMap 而不是 HashMap,但仍然得到随机结果。

我不能真正使用Collections.synchronizedMap(TreeMap),因为这会产生一个地图,我不能在上面使用 TreeMap API。

那么我还有哪些其他选择?

这是一个单线程环境。这只是一种主要方法。我怎样才能产生随机结果?

【问题讨论】:

  • 您在main() 中的测试是单线程的,因此没有并发,并且该缺陷很可能在实现本身中。在这种情况下,同步没有帮助。还请分析更多用例。例如,您的代码假定您在get(key,value) 中使用的每个键都存在,因为如果不存在,您将在CacheKey updatedKey = new CacheKey(searchKey.getCounter() + 1, .... 中获得NullPointerException,因为searchKeynull
  • 你好@Emmef。感谢您的答复。做出这个假设是因为两个映射中的键是一起更新的——这就是为什么我从来没有得到NullPointerException。我将测试放在一个循环中并迭代了一百万次。如果每次结果都不一样,怎么可能不是并发问题(?)
  • 您的代码没有启动任何线程,因此您只有一个线程。因此,没有其他线程可以并发访问您的代码,因为没有这样的线程。我承认我确实错过了外部this.cache.containsKey(key)) ;-) 如果你使用同步,你还必须包括这个外部检查。因为密钥可以在您的线程进入同步块之前被另一个线程删除,在那种的情况下,您得到一个NullPointerException
  • 我了解此实现的单线程性质,但对于随机结果,我想不出比并发想法更好的想法。
  • 由于您使用的是System.currentTimeMillis(),因此只有在某些运行中才有可能出现平局,因为这部分执行是不确定的。相反,请保持您自己的时钟在每次通话时都会提前以稳定您的测试。

标签: caching concurrency


【解决方案1】:

受到@BenManes 评论here 的启发,我开始思考System.currentTimeMillis() 是否是最好的选择,我发现了这篇文章here。我用System.nanoTime() 替换了System.currentTimeMillis()。现在,我不仅获得了一致的结果,而且获得了正确的结果。

【讨论】:

  • 您可能会喜欢阅读这个针对 LFU 的 alternative 策略,它的时间复杂度为 O(1),并且不使用系统时间。
猜你喜欢
  • 2010-10-31
  • 2023-04-07
  • 1970-01-01
  • 2017-04-12
  • 2019-04-01
  • 1970-01-01
  • 1970-01-01
  • 2019-05-11
  • 1970-01-01
相关资源
最近更新 更多