ThreadLocal HashMap vs ConcurrentHashMap用于线程安全的未绑定缓存

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

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

什么会有一个优越的性能,或在什么条件下一个解决方案优于另一个解决方案?

ThreadLocal HashMap:

class MyCache { private static class LocalMyCache { final Map map = new HashMap(); V get(K key) { V val = map.get(key); if (val == null) { val = computeVal(key); map.put(key, val); } return val; } } private final ThreadLocal localCaches = new ThreadLocal() { protected LocalMyCache initialValue() { return new LocalMyCache(); } }; public V get(K key) { return localCaches.get().get(key); } } 

ConcurrentHashMap的:

 class MyCache { private final ConcurrentHashMap map = new ConcurrentHashMap(); public V get(K key) { V val = map.get(key); if (val == null) { val = computeVal(key); map.put(key, val); } return val; } } 

我认为如果由于每个线程的所有缓存未命中而存在大量线程,ThreadLocal解决方案最初会变慢,但是超过数千个读取,摊销成本将低于ConcurrentHashMap解决方案。 我的直觉是否正确?

或者是否有更好的解决方案?

使用ThreadLocal作为缓存是一种不好的做法

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

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

如果你坚持,我建议使用周或软参考并在丰富的maxsize之后逐出

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

这个计算非常昂贵

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

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

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

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

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

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

 public interface Computable { V compute(K arg) throws InterruptedException; } public class Memoizer implements Computable { private final ConcurrentMap> cache = new ConcurrentHashMap>(); private final Computable c; public Memoizer(Computable c) { this.c = c; } public V compute(final K arg) throws InterruptedException { while (true) { Future f = cache.get(arg); if (f == null) { Callable eval = new Callable() { public V call() throws InterruptedException { return c.compute(arg); } }; FutureTask ft = new FutureTask(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()); } } } } 

鉴于实现这两者相对容易,我建议你尝试它们并在稳态负载下进行测试看看哪一个对你的应用程序表现最佳。

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

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

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

但是,您的主要问题是您不希望同一个键的并发计算; 所以每个键应该有一个锁; 这种锁通常可以通过ConcurrentHashMap实现。

所以我的解决方案就是

 class LazyValue { K key; volatile V value; V getValue() { lazy calculation, doubled-checked locking } } static ConcurrentHashMap 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; } 

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

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

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