12

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

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

显然这不是最好的设计,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);
}

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

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

我想要的是替换cacheMap将阻止获取给定键的获取,直到该键具有值,但同时允许其他获取。有没有这样的事情存在?或者有人有比 of 更清洁的替代品MapFutures

4

3 回答 3

11

为每个键创建一个锁听起来很诱人,但它可能不是您想要的,尤其是当键的数量很大时。

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

在这种情况下,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;
  }
}

直接使用 ConcurrentHashMap 确实有一个缺点,那就是多个线程可能会发现 key 没有被缓存,并且每个线程都可能调用 createFooExpensively()。因此,一些线程可能会做一次性工作。为避免这种情况,您需要使用“Java 并发实践”中提到的 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);
于 2013-05-30T06:43:02.710 回答
2

您可以使用funtom-java-utils - PerKeySynchronizedExecutor

它会为每把钥匙创建一个锁,但当它不用时会立即为您清除。

它还将授予具有相同键的调用之间的内存可见性,并且设计得非常快并最大限度地减少不同键的调用之间的争用。

在你的课堂上声明它:

final PerKeySynchronizedExecutor<KEY_CLASS> executor = new PerKeySynchronizedExecutor<>();

用它:

Foo foo = executor.execute(key, () -> createFooExpensively());
于 2016-07-17T09:04:23.137 回答
1
public class Cache {

    private static final Set<String> lockedKeys = new HashSet<>();

    private void lock(String key) {
        synchronized (lockedKeys) {
            while (!lockedKeys.add(key)) {
                try {
                    lockedKeys.wait();
                } catch (InterruptedException e) {
                    log.error("...");
                    throw new RuntimeException(e);
                }
            }
        }
    }

    private void unlock(String key) {
        synchronized (lockedKeys) {
            lockedKeys.remove(key);
            lockedKeys.notifyAll();
        }
    }

    public Foo getFromCache(String key) {
        try {
            lock(key);

            Foo result = cache.get(key);
            if (result == null) {
                result = createFooExpensively(key);
                cache.put(key, result);
            }
            return result;
            //For different keys it is executed in parallel.
            //For the same key it is executed synchronously.

        } finally {
            unlock(key);
        }
    }

}
  • key不仅可以是“String”,还可以是任何具有正确覆盖“equals”和“hashCode”方法的类。
  • try-finally - 非常重要 - 即使您的操作抛出异常,您也必须保证在操作后解锁等待线程。
  • 如果您的后端分布在多个服务器/JVM上,它将无法工作。
于 2019-02-13T15:30:05.487 回答