我正在创建一个具有以下特征的记忆缓存:
- 缓存丢失将导致计算和存储一个条目
- 这个计算非常昂贵
- 这个计算是幂等的
- 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。