我正试图解决一个简单的问题,并陷入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
或同步,这当然是不可能的。
你试图解决的问题被称为 安全出版物 并且存在 最佳性能解决方案的基准。就个人而言,我更喜欢持久性模式,它也表现最佳。定义一个 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模式。
要在Java中安全地发布非不可变对象,您需要同步对象的构造以及对该对象的共享引用的写入。 在这个问题中,重要的不仅仅是该对象的内部结构。
如果您在没有正确同步的情况下发布对象,并且重新排序,则使用者 Holder
如果在构造函数完成之前发布了对象的引用,则对象仍然可以看到部分构造的对象。例如 双重检查锁定 无 volatile
。
有几种方法可以安全地发布对象:
- 从静态初始化器初始化引用;
- 存储对它的引用
volatile
场或 AtomicReference
- 将对它的引用存储到正确构造的对象的最终字段中;要么
- 将对它的引用存储到由锁正确保护的字段中。
请注意,那些要点正在谈论对该的引用 Holder
对象,而不是类的字段。
所以最简单的方法是第一种选择:
public static Holder holder = new Holder("Some value");
访问静态字段的任何线程都将看到正确构造的 Holder
目的。
请参见第3.5.3节“安全发布习语” Java并发实践。有关不安全发布的更多信息,请参见第16.2.1节 Java并发实践。
看到 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;
}
}
无法保证非最终引用永远不会为空。
即使你正确地初始化它并保证在setter中不为null,
仍然可以通过反射将引用设置为null。
您可以通过将getter声明为final并且永远不会从getter返回null来限制返回null引用的机会。
它是;但是,仍然可以覆盖最终的getter并强制它返回null。这是一个描述如何模拟最终方法的链接: 最后的方法嘲笑
如果他们可以模拟最终方法,任何人都可以使用相同的技术覆盖最终方法并使其功能很差。