是否可以重新排序实例初始化和分配给共享变量?

我正在阅读一篇文章 ,其中涉及双重检查锁定,但我对作为示例提供的代码中更基本的失败感到惊讶。 在那里声明,实例的初始化(即,在构造函数返回之前发生的实例变量的写入)可能会对实例的引用写入共享变量(在静态字段中) 之后重新排序。以下示例)。

使用以下Foo类的定义是否正确,一个线程执行Foo.initFoo(); 和执行System.out.println(Foo.foo.a);的不同线程System.out.println(Foo.foo.a); ,第二个线程可能会打印0 (而不是1或抛出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 )?

调用foo = new Foo(); 涉及几个可能重新排序的操作,除非您引入适当的同步来防止它:

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

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

为了防止它你有几个解决方案,例如:

  • a决赛
  • 同步访问foo (使用同步的init AND getter)。

在不进入JLS#17的复杂性的情况下,您可以阅读关于类初始化的JLS#12.4.1 (强调我的):

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

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

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