你在问题中发布的示例来自Brian Goetz等人的"Java Concurrency In Practice"。它在3.2节“发布和逸出”中。我不会在这里尝试重现该部分的详细信息。(去买一本放在书架上,或者从同事那里借一本!)
示例代码所说明的问题是,在构造函数完成创建对象之前,允许引用正在构建的对象“逸出”。这有两个问题:
如果引用逸出,则在其构造函数完成初始化之前,某些内容可以使用该对象,并以不一致(部分初始化)的状态查看它。即使对象在初始化完成后逸出,声明子类也可能导致违反此规则。
根据JLS 17.5,对象的最终属性可以在没有同步的情况下安全使用。但是,只有在其构造函数完成之前未发布(未逸出)对象引用时,才成立这一点。如果您违反此规则,则结果是一个难以察觉的并发错误,当代码在多核/多处理器机器上执行时,可能会咬你。
< p >
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;
for (long l = 0; l < ; 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();
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”,表明主线程观察到由于通过“leak”数组进行不安全发布,导致“不可能”的状态的
Unsafe
对象。是否发生取决于您的JVM和硬件平台。现在,这个例子显然是人为的,但很容易想象这种情况如何在真正的多线程应用程序中发生。
当前Java内存模型是在Java 5中指定的(JLS的第3版),这是JSR 133的结果。在那之前,Java的与内存相关的方面没有明确定义。引用早期版本/版本的来源已经过时,但Goetz第1版中有关内存模型的信息是最新的。
内存模型的某些技术方面显然需要修订;请参见https://openjdk.java.net/jeps/188和https://www.infoq.com/articles/The-OpenJDK9-Revised-Java-Memory-Model/。然而,这项工作尚未出现在JLS修订版中。