9

我正在创建一个具有以下特征的记忆缓存:

  • 缓存未命中将导致计算和存储条目
    • 这种计算非常昂贵
    • 这个计算是幂等的
  • 无界(条目从未被删除),因为:
    • 输入将导致最多 500 个条目
    • 每个存储的条目都非常小
    • 缓存的寿命相对较短(通常不到一个小时)
    • 总的来说,内存使用不是问题
  • 将有数千次读取 - 在缓存的生命周期内,我预计 99.9%+ 的缓存命中率
  • 必须是线程安全的

什么会具有卓越的性能,或者在什么条件下一种解决方案比另一种更受青睐?

线程本地哈希映射:

class MyCache {
    private static class LocalMyCache {
        final Map<K,V> map = new HashMap<K,V>();

        V get(K key) {
            V val = map.get(key);
            if (val == null) {
                val = computeVal(key);
                map.put(key, val);
            }
            return val;
        }
    }

    private final ThreadLocal<LocalMyCache> localCaches = new ThreadLocal<LocalMyCache>() {
        protected LocalMyCache initialValue() {
            return new LocalMyCache();
        }
    };

    public V get(K key) {
        return localCaches.get().get(key);
    }
}

并发哈希映射:

class MyCache {
    private final ConcurrentHashMap<K,V> map = new ConcurrentHashMap<K,V>();

    public V get(K key) {
        V val = map.get(key);
        if (val == null) {
            val = computeVal(key);
            map.put(key, val);
        }
        return val;
    }
}

我认为如果有很多线程,因为每个线程的所有缓存未命中,ThreadLocal 解决方案最初会更慢,但是在数千次读取之后,摊销成本将低于 ConcurrentHashMap 解决方案。我的直觉正确吗?

还是有更好的解决方案?

4

6 回答 6

4

使用 ThreadLocal 作为缓存不是一个好习惯

在大多数容器中,线程通过线程池重用,因此永远不会被 gc。这会导致一些有线的东西

使用 ConcurrentHashMap 你必须管理它以防止内存泄漏

如果你坚持,我建议在丰富的 maxsize 之后使用 week 或 soft ref 和 evict

如果您正在寻找内存缓存解决方案(不要重新发明轮子)尝试番石榴缓存 http://docs.guava-libraries.googlecode.com/git/javadoc/com/google/common/cache/CacheBuilder.html

于 2013-01-12T15:37:09.940 回答
3

这种计算非常昂贵

我认为这是您创建缓存的原因,这应该是您最关心的问题。

虽然解决方案的速度可能略有不同 << 100 ns,但我怀疑能够在线程之间共享结果更为重要。即 ConcurrentHashMap 可能是最适合您的应用程序的,因为从长远来看,它可能会为您节省更多的 CPU 时间。

简而言之,与多次计算同一事物的成本(对于多个线程)相比,您的解决方案的速度可能很小

于 2013-01-12T15:49:36.157 回答
2

请注意,您的 ConcurrentHashMap 实现不是线程安全的,并且可能导致一个项目被计算两次。如果您直接存储结果而不使用显式锁定,那么要正确处理它实际上是相当复杂的,如果性能是一个问题,您当然希望避免这种情况。

值得注意的是,ConcurrentHashMap 具有高度可扩展性,并且在高争用情况下运行良好。我不知道 ThreadLocal 是否会表现得更好。

除了使用库之外,您还可以从Java 并发实践清单 5.19 中获得一些灵感。这个想法是将 a 保存Future<V>在您的地图中,而不是 a V。这有助于使整个方法线程安全,同时保持高效(无锁)。我粘贴下面的实现以供参考,但本章值得一读,以了解每个细节都很重要。

public interface Computable<K, V> {

    V compute(K arg) throws InterruptedException;
}

public class Memoizer<K, V> implements Computable<K, V> {

    private final ConcurrentMap<K, Future<V>> cache = new ConcurrentHashMap<K, Future<V>>();
    private final Computable<K, V> c;

    public Memoizer(Computable<K, V> c) {
        this.c = c;
    }

    public V compute(final K arg) throws InterruptedException {
        while (true) {
            Future<V> f = cache.get(arg);
            if (f == null) {
                Callable<V> eval = new Callable<V>() {
                    public V call() throws InterruptedException {
                        return c.compute(arg);
                    }
                };
                FutureTask<V> ft = new FutureTask<V>(eval);
                f = cache.putIfAbsent(arg, ft);
                if (f == null) {
                    f = ft;
                    ft.run();
                }
            }
            try {
                return f.get();
            } catch (CancellationException e) {
                cache.remove(arg, f);
            } catch (ExecutionException e) {
                throw new RuntimeException(e.getCause());
            }
        }
    }
}
于 2013-01-12T17:12:09.480 回答
1

性能问题无关紧要,因为解决方案并不相同。

ThreadLocal 哈希映射不在线程之间共享,因此甚至不会出现线程安全问题,但它也不符合您的规范,这并没有说明每个线程都有自己的缓存。

线程安全的要求意味着所有线程之间共享一个缓存,这完全排除了 ThreadLocal。

于 2013-01-13T00:18:35.670 回答
1

鉴于实现这两种方法相对容易,我建议您同时尝试它们并在稳态负载下进行测试,看看哪一种性能最适合您的应用程序。

我的猜测是 theConcurrentHashMap会快一点,因为它不必Thread.currentThread()像 a do 那样进行本机调用ThreadLocal。但是,这可能取决于您存储的对象以及它们的哈希编码的效率。

我可能也值得尝试将并发映射调整为concurrencyLevel您需要的线程数。默认为 16。

于 2013-01-12T15:42:07.640 回答
1

两种解决方案的查找速度可能相似。如果没有其他问题,我更喜欢 ThreadLocal,因为多线程问题的最佳解决方案是单线程。

但是,您的主要问题是您不希望对同一个键进行并发计算;所以每把钥匙都应该有一把锁;这样的锁通常可以通过 ConcurrentHashMap 来实现。

所以我的解决方案是

class LazyValue
{
    K key;

    volatile V value;

    V getValue() {  lazy calculation, doubled-checked locking }
}


static ConcurrentHashMap<K, LazyValue> centralMap = ...;
static
{
    for every key
        centralMap.put( key, new LazyValue(key) );
}


static V lookup(K key)
{
    V value = localMap.get(key);
    if(value==null)
        localMap.put(key, value=centralMap.get(key).getValue())
    return value;
}
于 2013-01-12T16:32:39.613 回答