当我在http://www.javaworld.com/article/2074979/java-concurrency/double-checked-locking--clever--but-broken.html上浏览一篇关于 DOUBLE-CHECKED LOCKING 的文章时 ,我遇到了一条评论它说“应该注意的是,实际上 DCL 可能在某些 JVM 的某些版本上工作——因为很少有 JVM 真正正确地实现了 JMM。”所以我从中推断 JMM 指定同步块是原子的,甚至是块在其他线程中不同步。我对吗?(我尝试阅读oracle网站上的JMM,但是太抽象了,我放弃了。)
1 回答
首先,请注意 Brian Goetz 于 2001 年撰写了这篇文章。在实施 JSR-133(修订后的内存模型)后,本文中描述的信息不再准确。然而,真实的是文章的示例 DCL 已损坏:
class SomeClass {
private Resource resource = null;
public Resource getResource() {
if (resource == null) {
synchronized (this) {
if (resource == null)
resource = new Resource();
}
}
return resource;
}
}
使用上面的代码,当实例的构造函数尚未完全执行时,resource
可能会观察到该字段不存在。null
问题是构造函数不能保证在字段分配之前执行,因为 JVM 可以应用代码优化。因此,构造函数调用应该被视为(在伪代码中):
resource = alloc Resource;
resource.new();
有了这些信息,人们就可以看到初始检查是如何为另一个线程resource == null
产生false
的,甚至在new
调用之前将不完整的实例暴露给另一个线程。这个其他线程永远不会进入同步块并且不会等待构造函数调用的完成。
然而,在今天的 Java 中,将resource
字段标记为volatile
. 在这种情况下,DCL 确实有效,甚至非常高效,因为在大多数硬件上读取 volatile 字段并不太昂贵。Alexey Shipilev 详细讨论了安全、懒惰发布的性能影响。DCL withvolatile
是当今的一种常见模式,例如被 Scala 用于其lazy
字段。
但是要回答您的实际问题:基本上 JVM 的所有实现都以比其规范更松散的方式实现内存模型。因此,尽管由于实现细节而导致不正确的同步,非易失性 DCL 可能只能在许多机器上工作。但是,您永远不应针对实现进行编码,而应始终针对规范进行编码。否则,您的代码可能只会在某些时候失败,而且只会在某些机器上失败,这是一个需要追踪的可怕错误!这与块的原子性无关synchronized
,它仅与您的 VM 如何执行您的代码有关,其中构造函数可能总是在将您的实例公开到该字段之前偶然执行。resource