繁体   English   中英

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

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

我正在尝试解决一个简单的问题,并陷入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;
  }
}

而根据这篇文章 ,标记字段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;
  }
}

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

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

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

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

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

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

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

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

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

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

您尝试解决的问题称为安全发布,并且存在最佳性能解决方案的基准 就个人而言,我更喜欢持久性模式,它也表现最佳。 使用单个通用字段定义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; }
}

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

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

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

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

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

注意 :不应将持有者模式与称为Holder示例类混淆。 Publisher者在我的示例中实现了holder模式。

请参阅Java语言规范的第17.5节

当构造函数完成时,对象被认为是完全初始化的。 在该对象完全初始化之后只能看到对象引用的线程可以保证看到该对象的最终字段的正确初始化值。

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

class Holder {

  private final Object ref;

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

  Object get() {
    return ref;
  }
}

如果你正在寻找一个非final字段,请认识到我们可以使用synchronized来强制get不会返回,直到ref为非null并且还确保在包装引用上保持正确的发生之前的关系(请参阅:memory barrier):

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,仍然可以通过反射将引用设置为null。

您可以通过将getter声明为final并且永远不会从getter返回null来限制返回null引用的机会。

它是; 但是,仍然可以覆盖最终的getter并强制它返回null。 这是一个描述如何模拟最终方法的链接: 最终方法模拟

如果他们可以模拟最终方法,任何人都可以使用相同的技术覆盖最终方法并使其功能很差。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM