【发布时间】: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;
}
我仍然得到随机结果
我尝试在 timeKeeper 和 cache 对象上进行同步,例如
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,因为searchKey是null。 -
你好@Emmef。感谢您的答复。做出这个假设是因为两个映射中的键是一起更新的——这就是为什么我从来没有得到
NullPointerException。我将测试放在一个循环中并迭代了一百万次。如果每次结果都不一样,怎么可能不是并发问题(?) -
您的代码没有启动任何线程,因此您只有一个线程。因此,没有其他线程可以并发访问您的代码,因为没有这样的线程。我承认我确实错过了外部
this.cache.containsKey(key));-) 如果你使用同步,你还必须包括这个外部检查。因为密钥可以在您的线程进入同步块之前被另一个线程删除,在那种的情况下,您将得到一个NullPointerException。 -
我了解此实现的单线程性质,但对于随机结果,我想不出比并发想法更好的想法。
-
由于您使用的是
System.currentTimeMillis(),因此只有在某些运行中才有可能出现平局,因为这部分执行是不确定的。相反,请保持您自己的时钟在每次通话时都会提前以稳定您的测试。
标签: caching concurrency