您在问题中发布的示例来自 Brian Goetz 等人的 "Java Concurrency In Practice"。它在第 3.2 节“发布和转义”中。我不会尝试在此处重现该部分的详细信息。 (去为你的书架买一本,或者从你的同事那里借一本!)
示例代码说明的问题是构造函数允许对正在构造的对象的引用在构造函数完成创建对象之前“转义”。这是一个问题有两个原因:
如果引用转义,则可以在其构造函数完成初始化之前使用该对象,并看到它处于不一致(部分初始化)状态。即使对象在初始化完成后转义,声明子类也可能导致违反这一点。
根据JLS 17.5,对象的最终属性可以在不同步的情况下安全使用。但是,仅当对象引用在其构造函数完成之前未发布(不转义)时才适用。如果您违反此规则,结果是一个潜在的并发错误,当代码在多核/多处理器机器上执行时,可能会困扰您。
ThisEscape 示例是偷偷摸摸的,因为引用通过隐式传递给匿名 EventListener 类构造函数的 this 引用转义。但是,如果过早地明确发布参考文献,也会出现同样的问题。
这里举个例子来说明对象初始化不完全的问题:
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”,表示由于通过leak 数组的不安全发布,主线程已经观察到Unsafe 对象处于“不可能”状态。这是否发生取决于您的 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 修订版中。