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

Arc*_*hie 11 java java-memory-model

我正在尝试解决一个简单的问题,并陷入Java内存模型兔子洞.

什么是最简单和/或最有效(判断调用此处),但无竞争(根据JMM精确定义)编写包含非最终引用字段的Java类的方法,该字段初始化为非空值构造函数,后来从未改变过,这样任何其他线程的后续访问都不能看到非空值?

破碎的起始示例:

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;
  }
}
Run Code Online (Sandbox Code Playgroud)

而根据这篇文章,标记该领域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;
  }
}
Run Code Online (Sandbox Code Playgroud)

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

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;
  }
}
Run Code Online (Sandbox Code Playgroud)

好的,这个怎么样?

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;
  }
}
Run Code Online (Sandbox Code Playgroud)

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

xia*_*.li 6

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

如果在没有正确同步的情况下发布对象,并且通过重新排序,Holder如果在构造函数完成之前发布了对象的引用,则对象的使用者仍然可以看到部分构造的对象.例如,双重检查锁定没有volatile.

有几种方法可以安全地发布对象:

  • 从静态初始化器初始化引用;
  • 将对它的引用存储到volatile字段或AtomicReference
  • 将对它的引用存储到正确构造的对象的最终字段中; 要么
  • 将对它的引用存储到由锁正确保护的字段中.

请注意,这些要点是讨论Holder对象的引用,而不是类的字段.

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

public static Holder holder = new Holder("Some value");
Run Code Online (Sandbox Code Playgroud)

访问静态字段的任何线程都将看到正确构造的Holder对象.

请参见实践Java Concurrency的第3.5.3节"安全发布惯用法" .有关不安全发布的更多信息,请参见" 实践中Java并发"一节中的第16.2.1节.


Raf*_*ter 5

您尝试解决的问题称为安全发布,并且存在最佳性能解决方案的基准.就个人而言,我更喜欢持久性模式,它也表现最佳.Publisher使用单个通用字段定义类:

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; }
}
Run Code Online (Sandbox Code Playgroud)

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

Holder holder = Publisher.publish(new Holder(value));
Run Code Online (Sandbox Code Playgroud)

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

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

请注意,这很好,因为现代VM在应用转义分析后会擦除对象分配.最小的性能开销来自生成的机器代码中的剩余内存障碍,但是安全地发布实例需要这些障碍.

注意:不要将持有者模式与被调用的示例类混淆Holder.它是Publisher在我的例子中实现持有者模式.