我正在阅读B. Goetz Java并发在实践中,现在我正在阅读section 3.5
有关安全的出版物。他说:
// Unsafe publication
public Holder holder;
public void initialize() {
holder = new Holder(42);
}
这种不适当的发布可能允许另一个线程观察部分构造的对象。
我不明白为什么可以观察到部分构造的子对象。假定构造函数Holder(int)
不允许this
转义。因此,构造的引用只能由调用者观察。现在,正如JLS 17.7所述:
引用的写入和读取始终是原子的,无论它们是实现为32位还是64位值。
线程不可能观察到部分构造的对象。
我哪里错了?
因此,构造的引用只能由调用者观察。
那就是您的逻辑被打破的地方,尽管这似乎是完全合理的话。
首先,第一件事:17.7提到的原子性仅表示当您阅读引用时,您将看到所有先前的值(从其默认值开始null
)或所有后续的值。您将永远不会获得带有对应于值1的某些位和对应于值2的某些位的引用,这实际上会使它成为对JVM堆中随机位置的引用-太糟糕了!他们基本上是说:“引用本身要么为null,要么指向内存中的有效位置。” 但是,那是什么,那在记忆中,事情可能会变得怪异。
我假设这个简单的Holder:
public class Holder {
int value; // NOT final!
public Holder(int value) { this.value = value; }
}
鉴于此,当您这样做时会发生什么holder = new Holder(42)
?
value = 0
)设置默认值<new instance>.value
为传入值(42)。Holder.holder
为此新引用问题在于,另一个线程可以按任何顺序查看这些事件,因为它们之间没有同步点。这是因为构造函数在语义之前没有任何特殊的同步或发生(这是个小谎言,但稍后会有更多介绍)。您可以在JLS 17.4.4上看到“与...同步”操作的完整列表;请注意,关于构造函数没有任何内容。
因此,另一个线程可能会看到这些操作按(1、3、2)排序。这意味着,如果在事件1和事件3之间安排了一些其他事件(例如,如果有人读Holder.holder.value
入本地var),那么他们将看到该新分配的对象,但在构造函数运行之前具有其值:您会看到Holder.holder.value == 0
。这称为部分构造的对象,可能会造成混乱。
如果构造函数有多个步骤(设置多个字段,或者先设置然后更改一个字段),则可以看到这些步骤的任何顺序。几乎所有赌注都没有了。kes!
final
领域上面我提到,当我断言构造函数没有任何特殊的同步语义时,我就撒谎了。假设您没有泄漏this
,则有一个例外:保证任何final
字段都可以在构造函数的末尾看到(请参阅JLS 17.5)。
您可以将其视为步骤2和3之间存在一种同步点,但它仅适用于final
字段。
final
字段访问的任何状态。因此,如果您有一个final List<String>
,并且您的构造函数对其进行了初始化,然后添加了一些值,那么可以保证所有线程都能看到该列表,至少具有其在构造函数末尾具有的状态,包括那些add
调用。(如果在构造函数之后修改列表,而没有同步,则所有选择都将关闭。)这就是为什么在我上面的示例中,确定value
最终结果很重要的原因。如果是这样,那么您将看不到Holder.holder.value == 0
。
本文收集自互联网,转载请注明来源。
如有侵权,请联系 [email protected] 删除。
我来说两句