Java 中同步的可见性影响

Visibility effects of synchronization in Java

This文章说:

In this noncompliant code example, the Helper class is made immutable by declaring its fields final. The JMM guarantees that immutable objects are fully constructed before they become visible to any other thread. The block synchronization in the getHelper() method guarantees that all threads that can see a non-null value of the helper field will also see the fully initialized Helper object.

public final class Helper {
  private final int n;

  public Helper(int n) {
    this.n = n;
  }

  // Other fields and methods, all fields are final
}

final class Foo {
  private Helper helper = null;

  public Helper getHelper() {
    if (helper == null) {            // First read of helper
      synchronized (this) {
        if (helper == null) {        // Second read of helper
          helper = new Helper(42);
        }
      }
    }

    return helper;                   // Third read of helper
  }
}

However, this code is not guaranteed to succeed on all Java Virtual Machine platforms because there is no happens-before relationship between the first read and third read of helper. Consequently, it is possible for the third read of helper to obtain a stale null value (perhaps because its value was cached or reordered by the compiler), causing the getHelper() method to return a null pointer.

我不知道该怎么办。我同意第一次和第三次阅读之间没有 happens before 关系,至少没有 immediate 关系。从某种意义上说,第一次读取必须在第二次之前发生,第二次读取必须在第三次之前发生,因此第一次读取必须在第三次之前发生,因此不存在传递发生之前的关系

谁能说的详细些?

不,这些读取之间没有任何传递关系。 synchornized 仅保证在同一锁的同步块内所做的更改的可见性。在这种情况下,所有读取都不使用同一锁上的同步块,因此这是有缺陷的,并且不能保证可见性。

因为字段初始化后没有锁定,所以声明字段很关键volatile。这将确保可见性。

private volatile Helper helper = null;

这里都解释清楚了https://shipilev.net/blog/2014/safe-public-construction/#_singletons_and_singleton_factories,问题很简单。

... Notice that we do several reads of instance in this code, and at least "read 1" and "read 3" are the reads without any synchronization ... Specification-wise, as mentioned in happens-before consistency rules, a read action can observe the unordered write via the race. This is decided for each read action, regardless what other actions have already read the same location. In our example, that means that even though "read 1" could read non-null instance, the code then moves on to returning it, then it does another racy read, and it can read a null instance, which would be returned!

没有,没有传递关系。

JMM 背后的想法是定义 JVM 必须遵守的规则。只要 JVM 遵循这些规则,它们就有权根据需要重新排序和执行代码。

在您的示例中,第二次读取和第三次读取不相关 - 例如,使用 synchronizedvolatile 不会引入内存障碍。因此,允许 JVM 按如下方式执行它:

 public Helper getHelper() {
    final Helper toReturn = helper;  // "3rd" read, reading null
    if (helper == null) {            // First read of helper
      synchronized (this) {
        if (helper == null) {        // Second read of helper
          helper = new Helper(42);
        }
      }
    }

    return toReturn; // Returning null
  }

您的调用将 return 为空值。然而,会创建一个单例值。但是,后续调用可能仍会得到空值。

如建议的那样,使用 volatile 会引入新的内存屏障。另一种常见的解决方案是捕获读取值并 return 它。

 public Helper getHelper() {
    Helper singleton = helper;
    if (singleton == null) {
      synchronized (this) {
        singleton = helper;
        if (singleton == null) {
          singleton = new Helper(42);
          helper = singleton;
        }
      }
    }

    return singleton;
  }

由于您依赖于局部变量,因此无需重新排序。一切都在同一个线程中发生。