java Java中的每键阻塞映射

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/16821767/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-11-01 00:05:18  来源:igfitidea点击:

Per-key blocking Map in Java

javaconcurrencyjava.util.concurrent

提问by David Moles

I'm dealing with some third-party library code that involves creating expensive objects and caching them in a Map. The existing implementation is something like

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

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

Obviously this is not the best design when Foosfor different keyscan be created independently.

显然,当Foosfor differentkeys可以独立创建时,这不是最好的设计。

My current hack is to use a Mapof Futures:

我目前的黑客是使用 a Mapof 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);
}

But this seems... a little hacky, for two reasons:

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

  1. The work is done on an arbitrary pooled thread. I'd be happy to have the work done on the first thread that tries to get that particular key, especially since it's going to be blocked anyway.
  2. Even when the Mapis fully populated, we still go through Future.get()to get the results. I expect this is pretty cheap, but it's ugly.
  1. 该工作是在任意池线程上完成的。我很高兴在第一个尝试获取该特定密钥的线程上完成工作,特别是因为它无论如何都会被阻止。
  2. 即使Map完全填充,我们仍然会通过Future.get()获取结果。我希望这很便宜,但它很丑陋。

What I'd like is to replace cachewith a Mapthat will block gets for a given keyuntil that key has a value, but allow other gets meanwhile. Does any such thing exist? Or does someone have a cleaner alternative to the Mapof Futures?

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

采纳答案by sjlee

Creating a lock per key sounds tempting, but it may not be what you want, especially when the number of keys is large.

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

As you would probably need to create a dedicated (read-write) lock for each key, it has impact on your memory usage. Also, that fine granularity may hit a point of diminishing returns given a finite number of cores if concurrency is truly high.

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

ConcurrentHashMap is oftentimes a good enough solution in a situation like this. It provides normally full reader concurrency (normally readers do not block), and updates can be concurrent up to the level of concurrency level desired. This gives you pretty good scalability. The above code may be expressed with ConcurrentHashMap like the following:

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

The straightforward use of ConcurrentHashMap does have one drawback, which is that multiple threads may find that the key is not cached, and each may invoke createFooExpensively(). As a result, some threads may do throw-away work. To avoid this, you would want to use the memoizer pattern that's mentioned in "Java Concurrency in Practice".

直接使用 ConcurrentHashMap 确实有一个缺点,那就是多个线程可能会发现 key 没有被缓存,每个线程都可能调用 createFooExpensively()。因此,某些线程可能会做一次性工作。为避免这种情况,您需要使用“Java 并发实践”中提到的 memoizer 模式。

But then again, the nice folks at Google already solved these problems for you in the form of CacheBuilder:

话说回来,谷歌的好人已经以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);

回答by Oz Molaim

You can use funtom-java-utils- PerKeySynchronizedExecutor.

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

It will create a lock for each key but will clear it for you immediately when it becomes unused.

它会为每个键创建一个锁,但会在它未使用时立即为您清除。

It will also grantee memory visibility between invocations with the same key, and is designed to be very fast and minimize the contention between invocations off different keys.

它还将授予具有相同键的调用之间的内存可见性,并且被设计为非常快并最小化不同键的调用之间的争用。

Declare it in your class:

在你的类中声明它:

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

Use it:

用它:

Foo foo = executor.execute(key, () -> createFooExpensively());

回答by Anton Fil

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

}
  • keycan be not only a 'String' but any class with correctly overridden 'equals' and 'hashCode' methods.
  • try-finally- is very important - you must guarantee to unlock waiting threads after your operation even if your operation threw exception.
  • It will not work if your back-end is distributed across multiple servers/JVMs.
  • key不仅可以是“String”,还可以是任何具有正确覆盖的“equals”和“hashCode”方法的类。
  • try-finally- 非常重要 - 即使您的操作抛出异常,您也必须保证在操作后解锁等待线程。
  • 如果您的后端分布在多个服务器/JVM 上,它将无法工作。