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


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

什么是最简单和/或最有效(判断调用此处),但无种族(根据JMM精确定义)编写包含a的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;
  }
}

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


10824
2017-08-08 22:29


起源

怎么样? AtomicReference? - Andy Turner
在哪种情况下的方法 getValue() 在第一个例子中可以返回 null ?此外,如果你计划在构造函数中只分配一次值然后只读它 - 听起来你正在寻找一个不可变对象,在这种情况下 - 为什么不将它声明为 final ? (我看到你写的是“非决赛”只是因为你没有提供好的动机...) - alfasin
getValue() 除非有一个,否则不能返回null this 逃避构造函数,没有。否则任何电话 getValue() 必须遵循整个构造函数,在构造函数返回后,在同一个线程或由同一个线程启动的线程中,并且 JLS#17.4; 5 担保 HB(X,Y) 如果 X 和 ÿ 是同一个线程中的连续动作。 - user207421
如果它在构造函数中初始化,并且从未更改过,为什么不将它作为最终版? - Bohemian♦
@Archie很对。但是 this 引用仅在构造函数返回后存在,并且将其提供给另一个线程的操作将构成 y在JLS报价方面。 - user207421


答案:


你试图解决的问题被称为 安全出版物 并且存在 最佳性能解决方案的基准。就个人而言,我更喜欢持久性模式,它也表现最佳。定义一个 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 通过a取消引用 final 在从相同的最终字段读取它之后,保证JMM完全初始化它。

如果这是您的类的唯一用法,那么您当然应该为您的类添加一个便利工厂并自己创建构造函数 private 避免不安全的施工。

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

注意:The 持有人模式 不要与被调用的示例类混淆 Holder。它是 Publisher 在我的例子中实现了holder模式。


5
2017-08-09 11:21



由于同样的原因,很可能确定这不起作用 volatile Holder v; Holder holder = v = new Holder(value); 不会。创建一个没有意义的东西 之前发生 单个线程中的操作之间的关系,因为它已经隐式存在。只有当另一个线程实际从中读取时,它才有效 final 要么 volatile 变量,如您链接的示例中所示。 - shmosel


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

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

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

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

请注意,那些要点正在谈论对该的引用 Holder 对象,而不是类的字段。

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

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

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

请参见第3.5.3节“安全发布习语” Java并发实践。有关不安全发布的更多信息,请参见第16.2.1节 Java并发实践


6
2017-08-09 00:15



好决定。安全发布是保证领域设定的方法。没有安全的出版物 Holder,甚至 synchronized 不会确保其领域的可见性;如果 volatile 是不够的,也是 synchronized。另一方面,如果 Holder 安全发布,有效不变,该领域不需要任何特殊处理。 - erickson


看到 Java语言规范的第17.5节

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

换句话说,只要我们小心不泄漏 this 来自的构造函数 Holder 到另一个线程,我们可以保证其他线程会看到正确的(非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 直到没有回来 ref 是非null并且还确保在包装引用上保持正确的发生之前的关系(请参阅:内存屏障):

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

2
2017-08-08 22:52



该问题明确指出“包含a的Java类 非最终 参考字段“,所以说它适用于最终字段不是问题的答案。甚至标题都说”非最终参考字段“。对我来说,这意味着OP知道它适用于最终字段,但是正在问如何让它适用于非最终领域。因此,这个答案是没有用的。 - Andreas
@Andreas好点;我已经更新了帖子 - oldrinb
这应该工作。但我怀疑是否有人在现实生活中以这种方式写课。 - xiaofeng.li
@LukeLee关心澄清? - oldrinb
恕我直言它只是花费太多。但为了什么?这样这个班级的用户将无法遵循安全的出版惯例吗? - xiaofeng.li


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

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

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

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

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


0
2017-08-08 22:59



也可以使用反射更改最终实例字段的值,但原始问题中没有这样的用例。 - PNS
这个问题不是关于黑客攻击对象,而是关于对对象的多线程访问。 - Andreas
这就是我所说的:原始问题中没有这样的用例。鉴于上述答案,我只是在反射与最终修改器上添加了更多信息。 - PNS