【问题标题】:Java: what is the correct way to guarantee a non-final reference field will never be read as null?Java:保证非最终引用字段永远不会被读取为空的正确方法是什么?
【发布时间】:2016-08-08 22:29:53
【问题描述】:

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

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

破碎的起始例子:

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 或同步的情况下执行此操作,这当然是不可能的。

【问题讨论】:

  • AtomicReference 怎么样?
  • 第一个示例中的方法getValue()在哪种情况下可以返回null?此外,如果您打算在构造函数中只分配一次值,然后就读取它 - 在这种情况下,听起来您正在寻找一个不可变对象 - 为什么不将其声明为 final ? (我看到你写“非最终”只是因为你没有提供好的动机......)
  • getValue() 不能返回 null,除非构造函数中有 this 转义,但没有。否则对getValue() 的任何调用都必须跟在整个构造函数之后,要么在同一个线程中,要么在构造函数返回后由同一个线程启动的线程中,并且JLS #17.4;5 保证hb(x,y)如果 xy 是同一线程中的连续动作。
  • 如果它在构造函数中被初始化,并且从未改变,为什么不让它成为final呢?
  • @Archie 非常正确。但是this引用只存在于构造函数返回后,将其交给另一个线程的动作将构成JLS引用中的y

标签: java java-memory-model


【解决方案1】:

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

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

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

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

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

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

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

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

请参阅Java Concurrency in Practice 的第 3.5.3 节“安全发布习惯用法”。有关不安全发布的更多信息,请参阅Java Concurrency in Practice 的第 16.2.1 节。

【讨论】:

  • 好电话。安全发布是保证字段设置的方式。没有Holder 的安全发布,即使synchronized 也无法确保其领域的可见性;如果volatile 不够用,synchronized 也不够用。另一方面,如果Holder 被安全发布,并且实际上是不可变的,则该字段不需要任何特殊处理。
【解决方案2】:

您要解决的问题称为安全发布,并且存在benchmarks for a best performant solution。就个人而言,我更喜欢性能最好的支架模式。使用单个通用字段定义 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 在从同一个 final 字段中读取它后保证将其完全初始化。

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

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

注意holder 模式不要与您的示例类被称为Holder 混淆。在我的示例中,实现持有者模式的是 Publisher

【讨论】:

  • 很确定这不起作用,出于同样的原因 volatile Holder v; Holder holder = v = new Holder(value); 不会。在单个线程中的操作之间创建 happens-before 关系是没有意义的,因为它已经隐式存在。它仅在其他线程实际从 finalvolatile 变量中读取时才有效,如您链接到的示例中所示。
【解决方案3】:

section 17.5 of the Java Language Specification

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

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

class Holder {

  private final Object ref;

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

  Object get() {
    return ref;
  }
}

如果您正在寻找一个非最终字段,请认识到我们可以使用synchronized 来强制getref 不为空之前不会返回,并确保正确发生之前的关系(参见:内存屏障)保留在包装的引用上:

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;
  }
}

【讨论】:

  • 该问题明确指出“包含 non-final 引用字段的 Java 类”,因此说它适用于 final 字段并不是问题的答案。甚至标题都说“非最终参考字段”。对我来说,这意味着 OP 知道它适用于最终字段,但正在询问如何使其适用于非最终字段。因此,这个答案没有用。
  • 这应该可以。但我怀疑在现实生活中真的有人这样写课程。
  • @LukeLee 想澄清一下?
  • 恕我直言,这太贵了。但为了什么?这样这个类的用户就不能不遵守安全发布习惯了?
【解决方案4】:

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

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

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

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

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

【讨论】:

  • 也可以通过反射改变最终实例字段的值,但原问题中没有这样的用例。
  • 这个问题不是关于破解对象,而是关于对对象的多线程访问。
  • 我也是这么说的:原始问题中没有这样的用例。鉴于上述答案,我只是添加了更多关于反射与最终修饰符的信息。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-08-23
  • 1970-01-01
  • 1970-01-01
  • 2023-03-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多