HashMap缓存中的同步

我有一个人们要求资源的网络应用程序。 为了提高效率,使用同步哈希映射缓存此资源。 这里的问题是当同时为同一个未缓存的资源发出两个不同的请求时:检索资源的操作会占用大量内存,所以我想避免为同一个资源多次调用它。

有人可以告诉我以下代码段是否存在任何潜在问题? 提前致谢。

private Map resources = Collections.synchronizedMap(new HashMap()); public void request(String name) { Resource resource = resources.get(name); if (resource == null) { synchronized(this) { if (resources.get(name) == null) { resource = veryCostlyOperation(name); // This should only be invoked once per resource... resources.put(resource); } else { resource = resources.get(name); } } } ... } 

一个可能的问题是您通过在synchronized块中执行veryCostlyOperation()来创建不必要的争用,因此许multithreading无法同时检索其(独立)资源。 这可以通过使用Future作为地图的值来解决:

 Map> map = new ConcurrentHashMap>(); ... Future r = map.get(name); if (r == null) { FutureTask task = null; synchronized (lock) { r = map.get(name); if (r == null) { task = new FutureTask(new Callable() { public Resource call() { return veryCostlyOperation(name); } }); r = task; map.put(name, r); } } if (task != null) task.run(); // Retrieve the resource } return r.get(); // Wait while other thread is retrieving the resource if necessary 

我看到的唯一潜在问题是你同步到this 。 如果同一类中的任何其他代码也与this同步,则只会立即运行其中一个块。 也许没有其他事情可以做到这一点,那很好。 不过,我总是担心下一个程序员要做什么。 (或者在我忘记这段代码的三个月内我自己)

我建议创建一个通用的同步对象,然后同步。

 private final Object resourceCreationSynchObject = new Object();

然后

 synchronized(this.resourceCreationSynchObject){
   ...
 }

否则,这完全符合您的要求。 它确保无法并行调用veryCostlyOperation

此外,在synchronized块中第二次重新获取资源是很好的想法。 这是必要的,并且外部的第一个调用确保在资源可用时不进行同步。 但没有理由第三次称呼它。 在synchronized块内部的第一件事,再次将resources.get(name)设置为resources.get(name) ,然后检查该变量是否为null。 这将阻止您在else子句中再次调用get

您的代码看起来没问题,除了您正在同步超过实际需要的代码:

  • 使用ConcurrentHashMap而不是同步的HashMap将允许多次调用get方法而不进行锁定。

  • 可能不需要this同步而不是resources ,但这取决于代码的其余部分。

您的代码可能会多次调用veryCostlyOperation(name)。 问题是查找地图后有一个不同步的步骤:

 public void request(String name) { Resource resource = resources.get(name); if (resource == null) { synchronized(this) { //... } } //... } 

映射中的get()由映射同步,但检查结果为null不受任何保护。 如果多个线程输入此请求相同的“名称”,则所有线程都将看到来自resources.get()的null结果,直到实际完成1,000Operation并将资源放入资源映射。

一种更简单,更有效但可扩展性更低的方法是使用法线贴图并使整个请求方法同步。 除非它在实践中实际上是一个问题,否则我会选择简单的方法。

为了获得更高的可伸缩性,您可以通过在synchronized(this)之后再次检查映射来修复代码,以捕获上面概述的情况。 它仍然不会提供最佳的可伸缩性,因为synchronized(this)只允许一个线程执行昂贵的操作,而在许多实际情况下,您只希望防止同一资源的多次执行,同时允许对不同资源的并发请求。 在这种情况下,您需要一些工具来同步所请求的资源。 一个非常基本的例子:

 private static class ResourceEntry { public Resource resource; } private Map resources = new HashMap(); public Resource request(String name) { ResourceEntry entry; synchronized (resources) { entry = resources.get(name); if (entry == null) { // if no entry exists, allocate one and add it to map entry = new ResourceEntry(); resources.put(name, entry); } } // at this point we have a ResourceEntry, but it *may* be no loaded yet synchronized (entry) { Resource resource = entry.resource; if (resource == null) { // must create the resource resource = costlyOperation(name); entry.resource = resource; } return resource; } } 

这只是一个粗略的草图。 基本上,它为ResourceEntry进行同步查找, 然后在ResourceEntry上进行同步,以确保特定资源仅构建一次