Java中的按键阻塞Map

Dav*_*les 10 java concurrency java.util.concurrent

我正在处理一些第三方库代码,涉及创建昂贵的对象并将其缓存在Map.现有的实现类似于

lock.lock()
try {
    Foo result = cache.get(key);
    if (result == null) {
        result = createFooExpensively(key);
        cache.put(key, result);
    }
    return result;
} finally {
    lock.unlock();
}
Run Code Online (Sandbox Code Playgroud)

显然,当Foos不同的keys可以独立创建时,这不是最好的设计.

我目前的黑客是使用以下Map方法Futures:

lock.lock();
Future<Foo> future;
try {
    future = allFutures.get(key);
    if (future == null) {
        future = executorService.submit(new Callable<Foo>() {
            public Foo call() {
                return createFooExpensively(key);
            }
        });
        allFutures.put(key, future);
    }
} finally {
    lock.unlock();
}

try {
    return future.get();
} catch (InterruptedException e) {
    throw new MyRuntimeException(e);
} catch (ExecutionException e) {
    throw new MyRuntimeException(e);
}
Run Code Online (Sandbox Code Playgroud)

但这似乎......有点hacky,原因有两个:

  1. 这项工作是在一个任意的汇集线程上完成的.我很乐意在尝试获取特定密钥的第一个线程上完成工作,特别是因为它无论如何都会被阻塞.
  2. 即使Map完全填充,我们仍然会Future.get()得到结果.我希望这很便宜,但它很难看.

我想要的是替换cache为一个Map将阻止获取给定键的键,直到该键具有值,但同时允许其他获取.有这样的事情吗?或者是否有人有对的清洁替代MapFutures

sjl*_*lee 9

为每个键创建锁定听起来很诱人,但它可能不是您想要的,特别是当键的数量很大时.

由于您可能需要为每个密钥创建一个专用(读写)锁,它会影响您的内存使用量.此外,如果并发性真的很高,那么精细粒度可能会在给定有限数量的核心的情况下达到收益递减点.

在这种情况下,ConcurrentHashMap通常是一个很好的解决方案.它通常提供完整的读者并发(通常读者不会阻塞),并且更新可以并发达到所需的并发级别.这为您提供了非常好的可扩展性.上面的代码可以用ConcurrentHashMap表示,如下所示:

ConcurrentMap<Key,Foo> cache = new ConcurrentHashMap<>();
...
Foo result = cache.get(key);
if (result == null) {
  result = createFooExpensively(key);
  Foo old = cache.putIfAbsent(key, result);
  if (old != null) {
    result = old;
  }
}
Run Code Online (Sandbox Code Playgroud)

直接使用ConcurrentHashMap确实有一个缺点,即多个线程可能会发现密钥没有被缓存,并且每个线程都可以调用createFooExpensively().结果,一些线程可能会丢弃工作.为避免这种情况,您可能希望使用"Java Concurrency in Practice"中提到的memoizer模式.

话说回来,Google的优秀人员已经以CacheBuilder的形式为您解决了这些问题:

LoadingCache<Key,Foo> cache = CacheBuilder.newBuilder().
  concurrencyLevel(32).
  build(new CacheLoader<Key,Foo>() {
    public Foo load(Key key) {
      return createFooExpensively(key);
    }
  });

...
Foo result = cache.get(key);
Run Code Online (Sandbox Code Playgroud)