您在问题中发布的示例来自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);
}
}
}
}
此应用程序的某些运行可能会打印“OUCH!”而不是“OK”,表示主线程由于通过数组进行不安全的发布而观察到对象处于“不可能”状态。这种情况是否发生将取决于您的JVM和硬件平台。Unsafe
leak
现在这个例子显然是人为的,但不难想象这种事情是如何在真正的多线程应用程序中发生的。
由于 JSR 133,当前的 Java 内存模型是在 Java 5(JLS 的第 3 版)中指定的。在此之前,Java的内存相关方面未被充分指定。引用早期版本/版本的源已过期,但Goetz版本1中有关内存模型的信息是最新的。
内存模型的一些技术方面显然需要修改;请参阅 https://openjdk.java.net/jeps/188 和 https://www.infoq.com/articles/The-OpenJDK9-Revised-Java-Memory-Model/。但是,这项工作尚未出现在JLS修订版中。