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

时间:2013-01-12 15:28:44

标签: java performance caching thread-local concurrenthashmap

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

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

什么会有优越的表现,或在什么条件下一种解决方案会优于另一种?

ThreadLocal HashMap:

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);
    }
}

的ConcurrentHashMap:

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解决方案。我的直觉是否正确?

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

6 个答案:

答案 0 :(得分:4)

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

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

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

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

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

答案 1 :(得分:3)

  

这个计算非常昂贵

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

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

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

答案 2 :(得分:2)

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

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

除了使用图书馆之外,您还可以从Java Concurrency in Practice Listing 5.19中获取灵感。我们的想法是在地图中保存Future<V>而不是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());
            }
        }
    }
}

答案 3 :(得分:1)

鉴于实现这两者相对容易,我建议您尝试两者并在稳态加载下测试以查看哪一个最适合您的应用。< / p>

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

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

答案 4 :(得分: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;
}

答案 5 :(得分:0)

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

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

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