我有一个缓存刷新逻辑,并希望确保它是线程安全且正确的方法。
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
调用的重要延迟?
答案 0 :(得分:3)
最好的方法是使用函数式编程范例,你有不可变状态(在这种情况下是Set
),而不是向该集添加和删除元素,每次都创建一个全新的Set
你想添加或删除元素。这是在Java9中。
然而,为遗留代码实现此方法可能有点尴尬或不可行。所以你可以做的就是有2 Sets
1,其上有get方法,它是volatile,然后在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
切换旧的Require Import Coq.Bool.Bool.
Goal forall b, b = true.
intros.
(* Create a large goal *)
do 300 rewrite <- orb_false_r with (b := b).
Time do 300 try idtac.
(* Finished transaction in 0.001 secs (0.004u,0.s) (successful) *)
Time do 300 try match goal with |- context [_ || true] => idtac end.
(* Finished transaction in 0.108 secs (0.108u,0.s) (successful) *)
Time do 300 try match goal with |- context [_ || ?b] => constr_eq b true end.
(* Finished transaction in 3.21 secs (3.208u,0.s) (successful) *)
Time do 300 try rewrite orb_true_r.
(* Finished transaction in 2.862 secs (2.863u,0.s) (successful) *)
,这就是为什么你只需要一个volatile变量来确保你不能同时读取和编辑缓存。
但正如我在一开始的回答中提到的那样,如果你从不修改集合,而是每次想要更新集合时再创建新集合(请注意,这是一个非常便宜的操作,因为内部旧集合被重用在操作中)。这样您就不必担心并发性,因为线程之间没有共享状态。
答案 1 :(得分:0)
在调用contains时,如何确保缓存不包含无效条目?此外,每次getNums()更改时都需要调用刷新,这非常低效。最好是确保控制对getNums()的更改,然后相应地更新缓存。缓存可能如下所示:
const itemsArray = list.map((item)=>{
if(item.done){
count+=1;
}
return (
<div> <li
key={item.index}
style={item.done?styleItem:null}
onClick={(e)=> this.props.toggleTodos(e,item.index) }
>{item.value}
</li>
<span
style={removeStyle}
onClick={(e)=> this.props.removeTodos(e,item.index)}
>x</span>
</div>
)
})
答案 2 :(得分:-1)
更新
正如@schmosel让我意识到的那样,我的努力是浪费的:实际上足以用HashSet<>
方法中的值初始化一个全新的refresh
。当然假设缓存标有volatile
。简而言之,@ Snickers3192的答案,指出你所寻求的。
旧答案
您也可以使用略有不同的系统。
保留两个Set<Integer>
,其中一个永远为空。刷新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];
}