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

时间:2015-12-30 07:31:33

标签: java multithreading

我正在阅读B. Goetz Java Concurrency在实践中,我现在正在section 3.5关于安全发布。他说:

// Unsafe publication
public Holder holder;
public void initialize() {
    holder = new Holder(42);
}
  

这个不正确的发布可能允许另一个线程观察到   部分构建的对象。

我不明白为什么可以观察到部分构建的子对象。假设构造函数Holder(int)不允许this转义。因此,构造的引用只能由调用者观察到。现在,正如JLS 17.7所述:

  

写入和读取引用始终是原子的,无论如何   是否将它们实现为32位或64位值。

线程无法观察部分构造的对象。

我哪里错了?

3 个答案:

答案 0 :(得分:18)

  

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

这就是你的逻辑中断的地方,虽然这似乎是一个非常合理的说法。

第一件事: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)时会发生什么?

  1. JVM为新的Holder对象分配一些空间,其所有字段的默认值(即value = 0
  2. JVM调用Holder构造函数
    • JVM将<new instance>.value设置为传入值(42)。
    • 构造函数完成
  3. JVM返回对我们刚分配的对象的引用,并将Holder.holder设置为此新引用
  4. 重新排序会让生活变得艰难(但它也会使程序变得快速!)

    问题是另一个线程可以按任何顺序查看这些事件,因为它们之间没有同步点。这是因为构造函数没有任何特殊的同步或发生在语义之前(这是轻微的谎言,但稍后会更多)。您可以在JLS 17.4.4看到“已同步”操作的完整列表;请注意,构造函数没有任何内容。

    因此,另一个线程可能会将这些操作命令为(1,3,2)。这意味着如果在事件1和3之间排序了某个其他事件 - 例如,如果有人将Holder.holder.value读入本地var - 那么他们将看到新分配的对象,但在构造函数运行之前已经看到了它的值:你会看到Holder.holder.value == 0。这被称为部分构造的对象,它可能非常混乱。

    如果构造函数有多个步骤(设置多个字段,或设置然后更改字段),那么您可以看到这些步骤的任何顺序。几乎所有赌注都已关闭。糟糕!

    构造函数和final字段

    我在上面提到过,当我断言构造函数没有任何特殊的同步语义时,我撒了谎。假设你没有泄漏this,那就有一个例外:任何final个字段 都可以保证看到它们位于构造函数的末尾(参见{{ 3}})。

    您可以将其视为步骤2和3之间存在某种同步点,但适用于final字段。

    • 它不适用于非最终字段
    • 它不会传递给其他同步点。
    • 会扩展到您通过final字段访问的任何州。所以,如果你有final List<String>,你的构造函数初始化它然后添加一些值,那么所有线程都保证看到该列表至少具有它在构造函数末尾的状态,包括那些{ {1}}来电。 (如果你在构造函数之后修改了列表,没有同步,那么所有的赌注都会再次关闭。)

    这就是为什么在我上面的例子中add不是最终的重要性。如果是,那么您将无法看到value

答案 1 :(得分:3)

Holder的构建大致包含三个部分:

  1. 分配内存
  2. 运行初始化程序
  3. 指定值字段holder
  4. 但是,出于性能原因,这些是重新排序的,可能会按如下方式运行:

    1. 分配内存
    2. 指定值字段holder
    3. 运行初始化程序
    4. 因此,已经可以将部分构造的对象分配给字段。从单线程角度来看,这没有任何问题。但从多线程的角度来看,这会导致明显的问题。

答案 2 :(得分:1)

  

现在,正如JLS 17.7所述,&#34;写入和读取引用始终是原子的......&#34;

这可能意味着比你认为的意思更少。这意味着,分配给参考变量的值永远不会被撕裂

如果某个线程更新了最初引用对象A的非易失性引用变量,将其更改为引用对象B,那么其他线程在检查变量时将看到A引用或B引用。没有线程会看到由旧值中的某些位和新值中的某些位组成的无效引用。

特别是,要求所有引用读写都是&#34; atomic&#34; not 是否意味着所有引用读取和写入都具有volatile语义。他们没有;如果一个线程更新非易失性引用变量以指向新构造的对象,则另一个线程在检查变量时可以获取新引用,但是看到对象本身处于部分初始化或未初始化状态。