刷新缓存而不影响访问缓存的延迟

问题描述 投票:2回答:3

我有一个缓存刷新逻辑,并希望确保它是线程安全和正确的方法来做到这一点。

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将被阻止。

我认为这段代码不够好,因为如果containclearaddAll之间调用,那么contain总是返回false。

在不影响contain调用的重要延迟的情况下,实现缓存刷新的最佳方法是什么?

java multithreading caching
3个回答
3
投票

最好的方法是使用函数式编程范例,其中你有不可变状态(在这种情况下是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变量来确保你不能同时读取和编辑缓存。

但正如我在一开始的回答中提到的那样,如果你从不修改集合,而是每次想要更新集合时都要创建新的集合(请注意,这是一个非常便宜的操作,因为旧的集合在操作中被重用) )。这样您就不必担心并发性,因为线程之间没有共享状态。


0
投票

在调用contains时,如何确保缓存不包含无效条目?此外,每次getNums()更改时都需要调用刷新,这非常低效。最好是确保控制对getNums()的更改,然后相应地更新缓存。缓存可能如下所示:

HashSet<>

-1
投票

更新

正如@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
© www.soinside.com 2019 - 2024. All rights reserved.