12

我有一段代码可以由多个线程执行,需要执行 I/O 绑定操作才能初始化存储在ConcurrentMap. 我需要使这个代码线程安全并避免不必要的调用来初始化共享资源。这是错误的代码:

    private ConcurrentMap<String, Resource> map;

    // .....

    String key = "somekey";
    Resource resource;
    if (map.containsKey(key)) {
        resource = map.get(key);
    } else {
        resource = getResource(key); // I/O-bound, expensive operation
        map.put(key, resource);
    }

使用上面的代码,多个线程可能会检查ConcurrentMap并看到资源不存在,并且都尝试调用getResource()代价高昂的资源。为了确保仅对共享资源进行一次初始化并在资源初始化后使代码高效,我想做这样的事情:

    String key = "somekey";
    Resource resource;
    if (!map.containsKey(key)) {
        synchronized (map) {
            if (!map.containsKey(key)) {
                resource = getResource(key);
                map.put(key, resource);
            }
        }
    }

这是双重检查锁定的安全版本吗?在我看来,由于调用了检查ConcurrentMap,它的行为就像一个被声明为的共享资源,volatile因此可以防止任何可能发生的“部分初始化”问题。

4

6 回答 6

4

如果您可以使用外部库,请查看 Guava 的MapMaker.makeComputingMap()。它是为您想要做的事情量身定制的。

于 2011-08-09T22:00:49.483 回答
3

是的,它是安全的。

如果map.containsKey(key)是真的,根据文档,map.put(key, resource)发生在它之前。因此getResource(key)事前发生resource = map.get(key),一切安然无恙。

于 2011-08-09T21:49:14.280 回答
2

为什么不在 ConcurrentMap 上使用 putIfAbsent() 方法?

if(!map.containsKey(key)){
  map.putIfAbsent(key, getResource(key));
}

可以想象,您可能会多次调用 getResource(),但它不会发生很多次。更简单的代码不太可能咬你。

于 2011-08-09T21:32:30.053 回答
1

通常,如果您正在同步的变量标记为 volatile ,则双重检查锁定是安全的。但是你最好同步整个函数:


public synchronized Resource getResource(String key) {
  Resource resource = map.get(key);
  if (resource == null) {
    resource = expensiveGetResourceOperation(key);    
    map.put(key, resource);
  }
  return resource;
}

对性能的影响很小,您可以确定不会出现同步问题。

编辑:

这实际上比其他方法更快,因为在大多数情况下您不必对地图进行两次调用。唯一的额外操作是空检查,其成本接近于零。

第二次编辑:

此外,您不必使用 ConcurrentMap。一个普通的 HashMap 就可以了。还是更快。

于 2011-08-09T22:03:36.193 回答
0

结论是。我以纳秒精度计时了 3 种不同的解决方案,因为毕竟最初的问题是关于性能的:

在常规 HashMap 上完全同步函数

synchronized (map) {

   Object result = map.get(key);
   if (result == null) {
      result = new Object();
      map.put(key, result);
   }                
   return result;
}

第一次调用:15,000 纳秒,后续调用:700 纳秒

使用带有 ConcurrentHashMap 的双重检查锁

if (!map.containsKey(key)) {
   synchronized (map) {
      if (!map.containsKey(key)) {
         map.put(key, new Object());
      }
   }
} 
return map.get(key);

第一次调用:15,000 纳秒,后续调用:1500 纳秒

双重检查 ConcurrentHashMap 的另一种风格

Object result = map.get(key);
if (result == null) {
   synchronized (map) {
      if (!map.containsKey(key)) {
         result = new Object();
         map.put(key, result);
      } else {
         result = map.get(key);
      }
   }
} 

return result;

第一次调用:15,000 纳秒,后续调用:1000 纳秒

您可以看到最大的成本是第一次调用,但所有 3 次调用都相似。随后的调用在常规 HashMap 上是最快的,使用方法同步,如 user237815 建议,但只有 300 NANO seocnds。毕竟我们在这里谈论的是 NANO 秒,这意味着 10 亿秒。

于 2012-08-17T15:37:46.903 回答
0

不需要 - ConcurrentMap使用其特殊的原子putIfAbsent方法支持这一点。

不要重新发明轮子:尽可能使用 API。

于 2011-08-09T21:33:55.687 回答