最新的 JMM 是否指定同步块对其他线程甚至异步线程都是原子的?
Does the latest JMM specify the synchronized block to be atomic to other threads even asynchronized ones?
当我在 http://www.javaworld.com/article/2074979/java-concurrency/double-checked-locking--clever--but-broken.html 上浏览一篇关于双重检查锁定的文章时,我遇到一条评论说“应该注意的是,事实上,DCL 可能适用于某些 JVM 的某些版本——因为很少有 JVM 真正正确地实现了 JMM。”
所以我从中推断,JMM 指定同步块是原子的,即使对于其他线程中未同步的块也是如此。
我对吗? (我试着看了oracle网站上的JMM,但是太抽象了,放弃了。)
首先,请注意 Brian Goetz 于 2001 年撰写了这篇文章。在 implementation of 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 字段是 not too expensive on most hardware. Alexey Shipilev has discussed the performance implications of safe, lazy publication in detail。 volatile
的 DCL 是当今的常见模式,例如 Scala 将其用于其 lazy
字段。
但要回答您的实际问题:基本上 JVM 的所有实现都以比其规范更宽松的方式实现内存模型。因此,尽管由于实现细节导致同步不正确,但非易失性 DCL 可能只在许多机器上工作。但是,您永远不应针对实现进行编码,而应始终针对规范进行编码。否则,您的代码可能只会偶尔失败,并且只会在某些机器上失败,这是一个很难追踪的错误!这与 synchronized
块是原子的无关,它仅与您的 VM 如何执行您的代码有关,其中构造函数可能 偶然地 总是在将您的实例公开给resource
字段。
当我在 http://www.javaworld.com/article/2074979/java-concurrency/double-checked-locking--clever--but-broken.html 上浏览一篇关于双重检查锁定的文章时,我遇到一条评论说“应该注意的是,事实上,DCL 可能适用于某些 JVM 的某些版本——因为很少有 JVM 真正正确地实现了 JMM。” 所以我从中推断,JMM 指定同步块是原子的,即使对于其他线程中未同步的块也是如此。 我对吗? (我试着看了oracle网站上的JMM,但是太抽象了,放弃了。)
首先,请注意 Brian Goetz 于 2001 年撰写了这篇文章。在 implementation of 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 字段是 not too expensive on most hardware. Alexey Shipilev has discussed the performance implications of safe, lazy publication in detail。 volatile
的 DCL 是当今的常见模式,例如 Scala 将其用于其 lazy
字段。
但要回答您的实际问题:基本上 JVM 的所有实现都以比其规范更宽松的方式实现内存模型。因此,尽管由于实现细节导致同步不正确,但非易失性 DCL 可能只在许多机器上工作。但是,您永远不应针对实现进行编码,而应始终针对规范进行编码。否则,您的代码可能只会偶尔失败,并且只会在某些机器上失败,这是一个很难追踪的错误!这与 synchronized
块是原子的无关,它仅与您的 VM 如何执行您的代码有关,其中构造函数可能 偶然地 总是在将您的实例公开给resource
字段。