[英]Double checked locking with ConcurrentMap
我有一段代码可以由多个线程执行,需要执行 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
,因此可以防止任何可能发生的“部分初始化”问题。
如果您可以使用外部库,请查看 Guava 的MapMaker.makeComputingMap() 。 它是为您想要做的事情量身定制的。
是的,它是安全的。
如果map.containsKey(key)
为真,根据文档, map.put(key, resource)
发生在它之前。 因此getResource(key)
发生在resource = map.get(key)
之前,一切正常。
为什么不在 ConcurrentMap 上使用 putIfAbsent() 方法?
if(!map.containsKey(key)){
map.putIfAbsent(key, getResource(key));
}
可以想象,您可能会多次调用 getResource(),但它不会发生很多次。 更简单的代码不太可能咬你。
通常,如果您正在同步的变量标记为 volatile,则双重检查锁定是安全的。 但是你最好同步整个 function:
public synchronized Resource getResource(String key) {
Resource resource = map.get(key);
if (resource == null) {
resource = expensiveGetResourceOperation(key);
map.put(key, resource);
}
return resource;
}
对性能的影响很小,您可以确定不会出现同步问题。
编辑:
这实际上比其他方法更快,因为在大多数情况下,您不必对 map 进行两次调用。 唯一的额外操作是 null 检查,其成本接近于零。
第二次编辑:
此外,您不必使用 ConcurrentMap。 一个普通的 HashMap 就可以了。 还是更快。
结论是。我以纳秒精度计时了 3 种不同的解决方案,因为毕竟最初的问题是关于性能的:
在常规 HashMap 上完全同步 function :
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 亿秒。
不需要 - ConcurrentMap使用其特殊的原子putIfAbsent方法支持这一点。
不要重新发明轮子:尽可能始终使用 API。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.