HashMap 缓存中的同步

发布于 2024-10-29 07:32:39 字数 682 浏览 5 评论 0原文

我有一个网络应用程序,人们可以在其中请求资源。为了提高效率,这些资源使用同步哈希图进行缓存。这里的问题是,当两个不同的请求同时针对同一未缓存的资源时:检索资源的操作会占用大量内存,因此我想避免对同一资源多次调用它。

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

private Map<String, Resource> resources = Collections.synchronizedMap(new HashMap<String, Resource>());

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

  ...

}

I've got a web application where people ask for resources. This resources are cached using a synchronized hash map for efficiency. The problem here is when two different requests come for the same uncached resource at the same time: the operation retrieving the resources takes up a lot of memory, so I want to avoid calling it more than once for the same resource.

Can somebody please tell me if there is any potential problem with the following snippet? Thanks in advance.

private Map<String, Resource> resources = Collections.synchronizedMap(new HashMap<String, Resource>());

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

  ...

}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

维持三分热 2024-11-05 07:32:39

一个可能的问题是,通过在 synchronized 块内执行 veryCostlyOperation() 会产生不必要的争用,从而导致许多线程无法同时检索其(独立)资源。这可以通过使用 Future 作为地图的值来解决:

Map<String, Future<Resource>> map = new ConcurrentHashMap<String, Future<Resource>>();    
...
Future<Resource> r = map.get(name);
if (r == null) {
    FutureTask task = null;
    synchronized (lock) {
        r = map.get(name);
        if (r == null) {
            task = new FutureTask(new Callable<Resource>() {
                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

One possible problem is that you create unnecessary contention by executing veryCostlyOperation() inside a synchronized block, so that many threads cannot retrieve their (independent) resources at the same time. This can be solved by using Future<Resource> as values of the map:

Map<String, Future<Resource>> map = new ConcurrentHashMap<String, Future<Resource>>();    
...
Future<Resource> r = map.get(name);
if (r == null) {
    FutureTask task = null;
    synchronized (lock) {
        r = map.get(name);
        if (r == null) {
            task = new FutureTask(new Callable<Resource>() {
                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
北渚 2024-11-05 07:32:39

我发现的唯一潜在问题是您同步到this。如果同一类中的任何其他代码也同步到 this,则只会同时运行这些块之一。也许没有其他东西可以做到这一点,那很好。不过,我总是担心下一个程序员会做什么。 (或者我自己在三个月内忘记了这段代码)

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

private final Object resourceCreationSynchObject = new Object();

否则

synchronized(this.resourceCreationSynchObject) {
  ...
}

,这正是您所要求的。它确保veryCostlyOperation不能被并行调用。

此外,在 synchronized 块中再次重新获取资源也是一个很好的想法。这是必要的,并且外部的第一次调用可确保您在资源已经可用时不会同步。但没有理由第三次调用它。在 synchronized 块中,首先将 resource 再次设置为 resources.get(name),然后检查该变量是否为 null。这将防止您必须在 else 子句中再次调用 get

The only potential problem I see is that you synchronize to this. If any other code in the same class also synchronizes to this, only one of those blocks will run at once. Maybe there's nothing else that does this, and that's fine. I always worry about what the next programmer is going to do, though. (or myself in three months when I've forgotten about this code)

I would recommend creating a generic synch object and then synch'ing to that.

private final Object resourceCreationSynchObject = new Object();

then

synchronized(this.resourceCreationSynchObject) {
  ...
}

Otherwise, this does exactly what you're asking for. It ensures that veryCostlyOperation cannot be called in parallel.

Also, it's great thinking to re-get the resource a second time within the synchronized block. This is necessary, and the first call outside makes sure that you don't synchronize when the resource is already available. But there's no reason to call it a third time. First thing inside the synchronized block, set resource again to resources.get(name) and then check that variable for null. That will prevent you from having to call get again inside the else clause.

楠木可依 2024-11-05 07:32:39

您的代码看起来不错,只是您同步的内容超出了实际需要的内容:

  • 使用 ConcurrentHashMap 而不是同步的 HashMap 将允许多次调用 get 方法而无需锁定.

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

Your code looks ok, except that you are synchronizing more than actually required:

  • Using a ConcurrentHashMap instead of a synchronized HashMap would allow multiple invocations of the get method without locking.

  • Synchronizing on this instead of resources is probably not necessary, but it depends on the rest of your code.

十秒萌定你 2024-11-05 07:32:39

您的代码可能会多次调用veryCostlyOperation(name)。问题在于,查找映射后存在一个不同步的步骤:

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

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

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

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

private static class ResourceEntry {
     public Resource resource;
}

private Map<String, ResourceEntry> resources = new HashMap<String, ResourceEntry>();

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 上进行同步,以确保特定资源仅构建一次。

Your code will potentially call veryCostlyOperation(name) multiple times. The problem is that there is an unsynchronized step after looking up the map:

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

The get() from the map is synchronized by the map, but checking the result for null is not protected by anything. If multiple threads enter this requesting the same "name", all of them will see a null result from resources.get(), until one actually finishes costlyOperation and puts the resource into the resources map.

A simpler and working, but less scalable approach would be to go with a normal map and make the entire request method synchronized. Unless it actually turns out a problem in practice I would choose the simple approach.

For higher scalability you can fix your code by checking the map again after synchronized(this) to catch the case outlined above. It would still not give the best scalability, since the synchronized(this) only allows one thread to execute costlyOperation, whereas in many practical cases, you only want to prevent multiple executions for the same resource while allowing for concurrent requests to different resources. In that case you need some facility to synchronize on the resource being requested. A very basic example:

private static class ResourceEntry {
     public Resource resource;
}

private Map<String, ResourceEntry> resources = new HashMap<String, ResourceEntry>();

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

This is only a rough sketch. Basically, it makes a sychronized lookup for a ResourceEntry, and then synchronizes on the ResourceEntry to ensure the specific resource is only built once.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文