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.
换句话说,只要我们注意不要将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
不会 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 模式。
我正在尝试解决一个简单的问题,但我掉进了 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.
换句话说,只要我们注意不要将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
不会 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 模式。