This文章说:
在此不兼容的代码示例中,Helper类变为不可变的 通过最终声明其字段。 JMM保证不可变 在其他对象看不见之前,对象已完全构建 线。 getHelper()方法中的块同步可确保 所有可以看到helper字段非空值的线程 还将看到完全初始化的Helper对象。
public final class Helper {
private final int n;
public Helper(int n) {
this.n = n;
}
// Other fields and methods, all fields are final
}
final class Foo {
private Helper helper = null;
public Helper getHelper() {
if (helper == null) { // First read of helper
synchronized (this) {
if (helper == null) { // Second read of helper
helper = new Helper(42);
}
}
}
return helper; // Third read of helper
}
}
但是,不能保证此代码在所有Java Virtual上都能成功 机器平台,因为没有事前发生的关系 在辅助程序的第一读和第三读之间。因此,这是 助手的三次读取有可能获得过时的null值 (可能是因为其值已由编译器缓存或重新排序), 导致getHelper()方法返回空指针。
我不知道该怎么做。我可以同意,在第一读和第三读之间没有任何关系,至少没有立即关系。在某种意义上说,第一读必须在第二之前发生,并且第二读必须在第三之前发生,所以第一读必须在第三之前发生
有人能更熟练地进行阐述吗?
答案 0 :(得分:3)
不,没有传递关系。
JMM背后的想法是定义JVM必须遵守的规则。只要JVM遵循这些规则,就可以根据需要授权它们重新排序和执行代码。
在您的示例中,第2次读取和第3次读取不相关-例如,使用synchronized
或volatile
不会引入存储障碍。因此,允许JVM执行以下操作:
public Helper getHelper() {
final Helper toReturn = helper; // "3rd" read, reading null
if (helper == null) { // First read of helper
synchronized (this) {
if (helper == null) { // Second read of helper
helper = new Helper(42);
}
}
}
return toReturn; // Returning null
}
您的呼叫将返回一个空值。但是,将创建一个单例值。但是,随后的调用仍可能会得到一个空值。
如所建议的,使用volatile将引入新的内存障碍。另一个常见的解决方案是捕获读取的值并将其返回。
public Helper getHelper() {
Helper singleton = helper;
if (singleton == null) {
synchronized (this) {
singleton = helper;
if (singleton == null) {
singleton = new Helper(42);
helper = singleton;
}
}
}
return singleton;
}
由于您依赖局部变量,因此无需重新排序。一切都在同一线程中发生。
答案 1 :(得分:2)
不,这些读段之间没有任何传递关系。 synchornized
仅保证可见在同一锁的同步块中所做的更改。在这种情况下,所有读取操作都不会在同一锁上使用同步块,因此存在缺陷并且无法保证可见性。
因为一旦初始化字段就没有锁定,所以将字段声明为volatile
是至关重要的。这样可以确保可见性。
private volatile Helper helper = null;
答案 2 :(得分:0)
问题https://shipilev.net/blog/2014/safe-public-construction/#_singletons_and_singleton_factories都在这里进行了解释,这个问题很简单。
...请注意,我们在这段代码中以及在 至少“ read 1”和“ read 3”是没有任何读数的读数 同步......规范规范,如之前所述 一致性规则,读取动作可以通过观察无序写入 比赛。这是针对每个读取操作决定的,无论其他操作如何 操作已读取相同的位置。在我们的示例中 表示即使“ read 1”可以读取非null实例,该代码 然后继续归还它,然后再读一遍, 可以读取一个空实例,该实例将被返回!