刷新缓存而不影响访问缓存的延迟
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了刷新缓存而不影响访问缓存的延迟相关的知识,希望对你有一定的参考价值。
我有一个缓存刷新逻辑,并希望确保它是线程安全和正确的方法来做到这一点。
public class Test {
Set<Integer> cache = Sets.newConcurrentHashSet();
public boolean contain(int num) {
return cache.contains(num);
}
public void refresh() {
cache.clear();
cache.addAll(getNums());
}
}
所以我有一个后台线程刷新缓存 - 定期调用refresh
。并且多个线程同时调用contain
。我试图避免在方法签名中使用synchronized
,因为refresh
可能需要一些时间(想象getNum
进行网络调用并解析大量数据)然后contain
将被阻止。
我认为这段代码不够好,因为如果contain
在clear
和addAll
之间调用,那么contain
总是返回false。
在不影响contain
调用的重要延迟的情况下,实现缓存刷新的最佳方法是什么?
最好的方法是使用函数式编程范例,其中你有不可变状态(在这种情况下是Set
),而不是添加和删除元素集,你每次想要添加或删除元素时创建一个全新的Set
。这是在Java9中。
然而,为遗留代码实现此方法可能有点尴尬或不可行。所以你可以做的就是有2个Sets
1,它有一个易失的get方法,然后在refresh
方法中为它分配一个新的实例。
public class Test {
volatile Set<Integer> cache = new HashSet<>();
public boolean contain(int num) {
return cache.contains(num);
}
public void refresh() {
Set<Integer> privateCache = new HashSet<>();
privateCache.addAll(getNums());
cache = privateCache;
}
}
编辑我们不想要或不需要ConcurrentHashSet
,也就是说你想同时在一个集合中添加和删除元素,这在我看来是一件非常无用的事情。但是你想用一个新的Set
切换旧的public class MyCache {
final ConcurrentHashMap<Integer, Boolean> cache = new ConcurrentHashMap<>(); //it's a ConcurrentHashMap to be able to use putIfAbsent
public boolean contains(Integer num) {
return cache.contains(num);
}
public void add(Integer nums) {
cache.putIfAbsent(num, true);
}
public clear(){
cache.clear();
}
public remove(Integer num) {
cache.remove(num);
}
}
,这就是为什么你只需要一个volatile变量来确保你不能同时读取和编辑缓存。
但正如我在一开始的回答中提到的那样,如果你从不修改集合,而是每次想要更新集合时都要创建新的集合(请注意,这是一个非常便宜的操作,因为旧的集合在操作中被重用) )。这样您就不必担心并发性,因为线程之间没有共享状态。
在调用contains时,如何确保缓存不包含无效条目?此外,每次getNums()更改时都需要调用刷新,这非常低效。最好是确保控制对getNums()的更改,然后相应地更新缓存。缓存可能如下所示:
HashSet<>
更新
正如@schmosel让我意识到的那样,我的努力是浪费的:事实上,用refresh
方法的值来初始化一个完整的新volatile
。当然假设缓存标有Set<Integer>
。简而言之,@ Snickers3192的答案,指出你所寻求的。
老答案
您也可以使用略有不同的系统。
保留两个cache
,其中一个永远是空的。刷新cache
时,可以异步重新初始化第二个,然后只需切换指针。访问缓存的其他线程将不会看到任何特定的开销。
从外部角度来看,他们将始终访问相同的private volatile int currentCache; // 0 or 1
private final Set<Integer> caches[] = new HashSet[2]; // use two caches; either one will always be empty, so not much memory consumed
private volatile Set<Integer> cachePointer = null; // just a pointer to the current cache, must be volatile
// initialize
{
this.caches[0] = new HashSet<>(0);
this.caches[1] = new HashSet<>(0);
this.currentCache = 0;
this.cachePointer = caches[this.currentCache]; // point to cache one from the beginning
}
。
public void refresh() {
// store current cache pointer
final int previousCache = this.currentCache;
final int nextCache = getNextPointer();
// you can easily compute it asynchronously
// in the meantime, external threads will still access the normal cache
CompletableFuture.runAsync( () -> {
// fill the unused cache
caches[nextCache].addAll(getNums());
// then switch the pointer to the just-filled cache
// from this point on, threads are accessing the new cache
switchCachePointer();
// empty the other cache still on the async thread
caches[previousCache].clear();
});
}
您的刷新方法可能如下所示:
public boolean contains(final int num) {
return this.cachePointer.contains(num);
}
private int getNextPointer() {
return ( this.currentCache + 1 ) % this.caches.length;
}
private void switchCachePointer() {
// make cachePointer point to a new cache
this.currentCache = this.getNextPointer();
this.cachePointer = caches[this.currentCache];
}
实用方法是:
qazxswpoi
以上是关于刷新缓存而不影响访问缓存的延迟的主要内容,如果未能解决你的问题,请参考以下文章