同时访问公共领域。为什么可以观察到不一致的状态?

圣安东尼奥:

我正在阅读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位值。

线程不可能观察到部分构造的对象。

我哪里错了?

yshavit:

因此,构造的引用只能由调用者观察。

那就是您的逻辑被打破的地方,尽管这似乎是完全合理的话。

首先,第一件事: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)

  1. JVM会为新的Holder对象分配一些空间,并为其所有字段(即value = 0设置默认值
  2. JVM调用Holder构造函数
    • JVM设置<new instance>.value为传入值(42)。
    • 构造函数完成
  3. JVM返回对我们刚刚分配的对象的引用,并设置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] 删除。

编辑于
0

我来说两句

0 条评论
登录 后参与评论

相关文章