您在问题中发布的示例来自Brian Goetz 等人的“Java Concurrency In Practice” 。它在第 3.2 节“发布和转义”中。我不会尝试在此处重现该部分的详细信息。(去为你的书架买一本,或者从你的同事那里借一本!)
示例代码说明的问题是构造函数允许对正在构造的对象的引用在构造函数完成创建对象之前“转义”。这是一个问题,原因有两个:
如果引用转义,则某些对象可以在其构造函数完成初始化之前使用该对象,并看到它处于不一致(部分初始化)状态。即使对象在初始化完成后转义,声明子类也可能导致违反这一点。
根据JLS 17.5,对象的最终属性可以在不同步的情况下安全使用。但是,仅当对象引用在其构造函数完成之前未发布(不转义)时才适用。如果你打破这个规则,结果是一个潜在的并发错误,当代码在多核/多处理器机器上执行时可能会咬你。
这个ThisEscape
例子是偷偷摸摸的,因为引用是this
通过隐式传递给匿名EventListener
类构造函数的引用转义的。但是,如果过早地明确发布参考文献,也会出现同样的问题。
下面举个例子来说明对象初始化不完全的问题:
public class Thing {
public Thing (Leaker leaker) {
leaker.leak(this);
}
}
public class NamedThing extends Thing {
private String name;
public NamedThing (Leaker leaker, String name) {
super(leaker);
}
public String getName() {
return name;
}
}
如果该Leaker.leak(...)
方法调用getName()
泄漏的对象,它将获得null
... 因为此时对象的构造函数链尚未完成。
这是一个示例来说明final
属性的不安全发布问题。
public class Unsafe {
public final int foo = 42;
public Unsafe(Unsafe[] leak) {
leak[0] = this; // Unsafe publication
// Make the "window of vulnerability" large
for (long l = 0; l < /* very large */ ; l++) {
...
}
}
}
public class Main {
public static void main(String[] args) {
final Unsafe[] leak = new Unsafe[1];
new Thread(new Runnable() {
public void run() {
Thread.yield(); // (or sleep for a bit)
new Unsafe(leak);
}
}).start();
while (true) {
if (leak[0] != null) {
if (leak[0].foo == 42) {
System.err.println("OK");
} else {
System.err.println("OUCH!");
}
System.exit(0);
}
}
}
}
此应用程序的某些运行可能会打印“哎哟!” 而不是“OK”,表示由于通过数组Unsafe
的不安全发布,主线程已经观察到对象处于“不可能”状态。leak
这是否发生取决于您的 JVM 和硬件平台。
现在这个例子显然是人为的,但是不难想象这种事情在真正的多线程应用程序中怎么会发生。
作为 JSR 133 的结果,Java 5(JLS 第 3 版)中指定了当前的 Java 内存模型。在此之前,Java 与内存相关的方面未得到充分说明。引用早期版本/版本的来源已过时,但 Goetz 版本 1 中有关内存模型的信息是最新的。
内存模型的一些技术方面显然需要修改;请参阅https://openjdk.java.net/jeps/188和https://www.infoq.com/articles/The-OpenJDK9-Revised-Java-Memory-Model/。然而,这项工作尚未出现在 JLS 修订版中。