【问题标题】:Per-key blocking Map in JavaJava中的每键阻塞映射
【发布时间】:2013-05-29 18:56:30
【问题描述】:

我正在处理一些第三方库代码,这些代码涉及创建昂贵的对象并将它们缓存在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 时,显然这不是最好的设计。

我目前的技巧是使用MapFutures

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

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

  1. 工作在任意池化线程上完成。我很高兴有这份工作 在尝试获取该特定密钥的第一个线程上完成,尤其是因为 无论如何它都会被阻止。
  2. 即使Map 已完全填充,我们仍然通过Future.get() 来获取 结果。我认为这很便宜,但它很丑。

我想要的是用Map 替换cache,这将阻止获取给定键的,直到该键具有值,但同时允许其他获取。有没有这样的东西存在?或者有人有比MapFutures 更干净的替代品吗?

【问题讨论】:

  • 将关键对象存储在ConcurrentHashMap 中并锁定关键对象本身?如果键是内在函数(int、String 等),则包装它们。
  • 这听起来就像你想要一个 Guava Striped,它充当从对象到锁的映射。

标签: java concurrency java.util.concurrent


【解决方案1】:

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

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

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

【讨论】:

  • 谢谢——JCiP 中的Memoizer 部分几乎完全介绍了这种情况。我猜应该有 RTFB。 :)
  • (当我回到我们自己的代码并访问 Guava 时,我会记得 CacheBuilder。)
  • 有人解除了这个答案并创建了一篇博文newbedev.com/per-key-blocking-map-in-java
【解决方案2】:

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

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

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

在你的课堂上声明:

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

使用它:

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

【讨论】:

    【解决方案3】:
    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上,它将无法工作。

    【讨论】:

      猜你喜欢
      • 2010-12-13
      • 2017-10-06
      • 1970-01-01
      • 2018-01-04
      • 1970-01-01
      • 2014-01-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多