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



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

  • 缓存丢失将导致计算和存储一个条目
    • 这个计算非常昂贵
    • 这个计算是幂等的
  • unbounded(条目从未被删除):
    • 输入将导致最多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解决方案。我的直觉对吗?

还是有更好的解决方案?

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

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

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

如果你坚持,我建议在rich maxsize

之后使用week或soft reft和evt。

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

这个计算非常昂贵

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

虽然解决方案的速度可能略有不同<<在100秒内,我认为更重要的是能够在线程之间共享结果。也就是说,ConcurrentHashMap可能是最适合你的应用程序的,因为从长远来看,它可能为你节省更多的CPU时间。

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

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

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

除了使用库之外,还可以从实践清单5.19中的Java并发性中获得一些灵感。这个想法是在你的地图中保存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());
            }
        }
    }
}

考虑到实现这两个相对容易,我建议您都尝试一下,并在稳态负载下测试,看看哪一个对您的应用程序表现最好。

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

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

两种解决方案中的查找速度可能相似。如果没有其他问题,我更喜欢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;
}

性能问题是不相关的,因为解决方案是不相等的。

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

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

最新更新