使用ConcurrentMap双重检查锁定

时间:2011-08-09 21:27:24

标签: java concurrency synchronization double-checked-locking

我有一段代码可以由多个线程执行,这些线程需要执行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的共享资源,因此可以防止可能发生的任何“部分初始化”问题。

6 个答案:

答案 0 :(得分:4)

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

答案 1 :(得分:3)

是的,'安全。

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

答案 2 :(得分:2)

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

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

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

答案 3 :(得分: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会做到这一点。更快。

答案 4 :(得分:0)

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

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

答案 5 :(得分: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秒,这意味着一个十亿秒。