13

我正在阅读一篇文章,其中实际上谈到了双重检查锁定,但我对作为示例的代码中更基本的失败感到惊讶。那里指出,实例的初始化(即在构造函数返回之前写入实例变量)可能会对实例的引用写入共享变量(在下面的例子)。

Foo使用以下 class 定义,一个线程正在执行Foo.initFoo();,另一个线程正在执行System.out.println(Foo.foo.a);,第二个线程可能会打印0(而不是1或抛出 a NullPointerException),这是真的吗?

class Foo {
    public int a = 1;

    public static Foo foo;

    public static void initFoo() {
        foo = new Foo();
    }

    public static void thread1() {
        initFoo(); // Executed on one thread.
    }

    public static void thread2() {
        System.out.println(foo.a); // Executed on a different thread
    }
}

从我对 Java 内存模型(以及其他语言的内存模型)的了解来看,这实际上并不让我感到惊讶,但直觉强烈认为这是不可能的(可能是因为涉及对象初始化并且对象初始化似乎如此在 Java 中是神圣的)。

0是否可以在第一个线程中不同步的情况下“修复”此代码(即它永远不会打印)?

4

2 回答 2

7

调用foo = new Foo();涉及几个操作,除非您引入适当的同步以防止它发生,否则这些操作可能会被重新排序:

  1. 为新对象分配内存
  2. 写入字段的默认值 ( a = 0)
  3. 写入字段的初始值 ( a = 1)
  4. 发布对新创建对象的引用

如果没有适当的同步,步骤 3 和 4 可能会重新排序(请注意,步骤 2 必然发生在步骤 4 之前),尽管在 x86 架构上的热点不太可能发生。

为了防止它,您有几种解决方案,例如:

  • a最后
  • 同步访问foo(使用同步的initAND getter)。

无需深入了解 JLS #17 的复杂性,您可以阅读JLS #12.4.1关于类初始化(强调我的):

初始化代码不受限制的事实允许构造示例,其中在评估其初始化表达式之前,类变量的值仍然具有初始默认值时可以观察到,但这样的示例在实践中很少见。(这样的例子也可以构造实例变量初始化。)Java 编程语言的全部功能在这些初始化程序中可用;程序员必须小心谨慎。这种能力给代码生成器带来了额外的负担,但无论如何都会出现这种负担,因为 Java 编程语言是并发的。

于 2013-04-15T09:07:28.260 回答
3

即使在 x86 下,也可以通过 JIT 编译器对实例初始化重新排序。但是,编写可以触发这种重新排序的代码有些棘手。关于如何重现这种重新排序,请参阅我的问题:

Hotspot JIT 编译器是否有任何可以重现的指令重新排序?

于 2016-03-11T07:54:52.373 回答