Java:保证非最终引用字段永远不会被读取为空的正确方法是什么?

Java: what is the correct way to guarantee a non-final reference field will never be read as null?

我正在尝试解决一个简单的问题,但我掉进了 Java 内存模型兔子洞。

什么是最简单的and/or最有效的(此处判断调用),但无竞争(根据JMM精确定义)的写Java class包含一个non-final 引用字段,它在构造函数中被初始化为一个非空值,并且随后从未改变,这样任何其他线程对该字段的后续访问都不会看到一个非空值值?

错误的开始示例:

public class Holder {

  private Object value;

  public Holder(Object value) {
    if (value == null)
        throw NullPointerException();
    this.value = value;
  }

  public Object getValue() {    // this could return null!
    return this.value;
  }
}

并且根据 this post,标记字段 volatile 甚至不起作用!

public class Holder {

  private volatile Object value;

  public Holder(Object value) {
    if (value == null)
        throw NullPointerException();
    this.value = value;
  }

  public Object getValue() {    // this STILL could return null!!
    return this.value;
  }
}

这是我们能做的最好的了吗?

public class Holder {

  private Object value;

  public Holder(Object value) {
    if (value == null)
        throw NullPointerException();
    synchronized (this) {
        this.value = value;
    }
  }

  public synchronized Object getValue() {
    return this.value;
  }
}

好的,这个怎么样?

public class Holder {

  private Object value;

  public Holder(Object value) {
    if (value == null)
        throw NullPointerException();
    this.value = value;
    synchronized (this) { }
  }

  public synchronized Object getValue() {
    return this.value;
  }
}

旁注:related question 询问如何在不使用任何 volatile 或同步的情况下执行此操作,这当然是不可能的。

section 17.5 of the Java Language Specification

An object is considered to be completely initialized when its constructor finishes. A thread that can only see a reference to an object after that object has been completely initialized is guaranteed to see the correctly initialized values for that object's final fields.

换句话说,只要我们注意不要将thisHolder的构造函数泄漏到另一个线程,我们就可以保证其他线程会看到正确的(非null) ref 的值,没有额外的同步机制。

class Holder {

  private final Object ref;

  Holder(final Object obj) {
    if (obj == null) {
      throw new NullPointerException();
    }
    ref = obj;
  }

  Object get() {
    return ref;
  }
}

如果您正在寻找 非最终 字段,请认识到我们可以使用 synchronized 强制执行 get 不会 return直到 ref 为非空,并确保正确的 happens-before 关系(参见:内存屏障)保持包装引用:

class Holder {

  private Object ref;

  Holder(final Object obj) {
    if (obj == null) {
      throw new NullPointerException();
    }
    synchronized (this) {
      ref = obj;
      notifyAll();
    }
  }

  synchronized Object get() {
    while (ref == null) {
      try {
        wait();
      } catch (final InterruptedException ex) { }
    }
    return ref;
  }
}

无法保证非最终引用永远不会为空。

即使你正确初始化它并保证在setter中不为空, 仍然可以通过反射将引用设置为 null。

您可以通过声明 getter 是最终的并且从不从 getter 中 returning null 来限制 returning 空引用的机会。

是的;但是,仍然可以覆盖最终的 getter 并将其强制为 return null。这是描述如何模拟最终方法的 link:Final method mocking

如果他们可以模拟 final 方法,任何人都可以使用相同的技术覆盖 final 方法并使其运行不佳。

要在 Java 中安全地发布一个非不可变对象,您需要同步对象的构造和对该对象的共享引用的写入。在这个问题中重要的不仅仅是那个对象的内部结构。

如果您在没有适当同步的情况下发布一个对象,重新排序,如果在构造函数完成之前发布了对对象的引用,Holder 对象的使用者仍然可以看到部分构造的对象。例如 Double-checked locking 没有 volatile.

有几种安全发布对象的方法:

  • 正在从静态初始化程序初始化引用;
  • 将对它的引用存储到 volatile 字段或 AtomicReference
  • 将对它的引用存储到正确构造的对象的最终字段中;或者
  • 将对它的引用存储到一个由锁适当保护的字段中。

请注意,这些要点是指对 Holder 对象的引用,而不是 class 的字段。

所以最简单的方法是第一个选项:

public static Holder holder = new Holder("Some value");

任何访问静态字段的线程都会看到一个正确构造的 Holder 对象。

参见 Java Concurrency in Practice. For more information about unsafe publication, see Section 16.2.1 of Java Concurrency in Practice 的第 3.5.3 节 "Safe publication idioms"。

您要解决的问题叫做安全发布并且存在benchmarks for a best performant solution。就个人而言,我更喜欢表现最好的支架图案。使用单个通用字段定义 Publisher class:

class Publisher<T> {
  private final T value;
  private Publisher(T value) { this.value = value; }
  public static <S> S publish(S value) { return new Publisher<S>(value).value; }
}

您现在可以通过以下方式创建您的实例:

Holder holder = Publisher.publish(new Holder(value));

由于您的 Holder 是通过 final 字段取消引用的,因此保证在从相同的最终字段读取后由 JMM 完全初始化。

如果这是您的 class 的唯一用途,那么您当然应该为您的 class 添加一个便利工厂,并使构造函数本身 private 以避免不安全的构造。

请注意,这表现得非常好,因为现代虚拟机在应用逃逸分析后擦除对象分配。最小的性能开销来自生成的机器代码中剩余的内存屏障,但是安全发布实例需要这些内存屏障。

注意holder pattern不要与你的例子class被调用Holder混淆.在我的示例中,Publisher 实现了 holder 模式。