因此,构造的引用只能由调用者观察到。
这就是你的逻辑崩溃的地方,尽管这似乎是完全合理的说法。
首先要做的事情是:17.7 提到的原子性仅表示当您读取引用时,您将看到所有先前的值(从其默认值开始)null
)或所有后续值。您永远不会获得一些位对应于值 1 和一些位对应于值 2 的引用,这实际上会使其成为 JVM 堆中随机位置的引用 - 这将是可怕的!基本上他们是说,“引用本身要么为空,要么指向内存中的有效位置。”但什么是in那个记忆,就是事情变得奇怪的地方。
设置一个简单的例子
我假设这个简单的持有者:
public class Holder {
int value; // NOT final!
public Holder(int value) { this.value = value; }
}
鉴于此,当你这样做时会发生什么holder = new Holder(42)
?
- JVM 为新的 Holder 对象分配一些空间,并为其所有字段分配默认值(即
value = 0
)
- the JVM invokes the Holder constructor
- JVM 设置
<new instance>.value
到传入值 (42)。
- 构造函数完成
- JVM 返回对我们刚刚分配的对象的引用,并设置
Holder.holder
到这个新的参考
重新排序使事情变得困难(但它也使程序变得更快!)
问题是另一个线程可以按任意顺序查看这些事件,因为它们之间没有同步点。这是因为构造函数没有任何特殊的同步或发生之前语义(这是一个小谎言,稍后会详细介绍)。您可以在以下位置查看“同步”操作的完整列表:JLS 17.4.4 http://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4.4;请注意,其中没有关于构造函数的内容。
因此,另一个线程可能会看到这些操作的顺序为 (1, 3, 2)。这意味着如果某个其他事件在事件 1 和 3 之间排序——例如,如果有人读Holder.holder.value
到本地变量中——然后他们会看到新分配的对象,但在构造函数运行之前显示它的值:你会看到Holder.holder.value == 0
。这称为部分构造的对象,它可能非常令人困惑。
如果构造函数有多个步骤(设置多个字段,或设置然后更改字段),那么您可以看到这些步骤的任何顺序。几乎所有的赌注都落空了。哎呀!
构造函数和final
fields
我上面提到,当我断言构造函数没有任何特殊的同步语义时,我撒了谎。假设你不泄露this
,有一个例外:任何final
fields are保证被视为位于构造函数末尾(请参阅JLS 17.5 http://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.5).
您可以将其视为步骤 2 和步骤 3 之间存在某种同步点,但它only适用于final
fields.
- 它不适用于非最终字段
- 它不会传递到其他同步点。
- It does但是,可以扩展到您通过以下方式访问的任何状态
final
字段。所以,如果你有一个final List<String>
,并且您的构造函数初始化它,然后添加一些值,然后保证所有线程都能看到该列表,至少具有它在构造函数末尾的状态,包括那些add
来电。 (如果您在构造函数之后修改列表而不进行同步,那么所有的赌注都会再次取消。)
这就是为什么它在我上面的例子中很重要value
不是最终的。如果是的话,你就看不到了Holder.holder.value == 0
.