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