最新的 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 detailvolatile 的 DCL 是当今的常见模式,例如 Scala 将其用于其 lazy 字段。

但要回答您的实际问题:基本上 JVM 的所有实现都以比其规范更宽松的方式实现内存模型。因此,尽管由于实现细节导致同步不正确,但非易失性 DCL 可能只在许多机器上工作。但是,您永远不应针对实现进行编码,而应始终针对规范进行编码。否则,您的代码可能只会偶尔失败,并且只会在某些机器上失败,这是一个很难追踪的错误!这与 synchronized 块是原子的无关,它仅与您的 VM 如何执行您的代码有关,其中构造函数可能 偶然地 总是在将您的实例公开给resource 字段。