是否可以重新排序实例初始化和分配给共享变量?
我正在阅读一篇文章 ,其中涉及双重检查锁定,但我对作为示例提供的代码中更基本的失败感到惊讶。 在那里声明,实例的初始化(即,在构造函数返回之前发生的实例变量的写入)可能会在对实例的引用写入共享变量(在静态字段中) 之后重新排序。以下示例)。
使用以下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();
涉及几个可能重新排序的操作,除非您引入适当的同步来防止它:
- 为新对象分配内存
- 写字段的默认值(
a = 0
) - 写字段的初始值(
a = 1
) - 发布对新创建的对象的引用
如果没有正确的同步,可能会重新排序步骤3和4(请注意,步骤2必须在步骤4之前发生),尽管x86架构上的热点不太可能发生。
为了防止它你有几个解决方案,例如:
- 做
a
决赛 - 同步访问
foo
(使用同步的init
AND getter)。
在不进入JLS#17的复杂性的情况下,您可以阅读关于类初始化的JLS#12.4.1 (强调我的):
初始化代码不受限制的事实允许构造示例,其中在其初始化表达式被评估之前,当它仍然具有其初始默认值时可以观察到类变量的值 ,但是这样的示例在实践中是罕见的。 ( 这些示例也可以构造为例如变量初始化 。)这些初始化器中可以使用Java编程语言的全部function。 程序员必须小心谨慎。 这种能力给代码生成器带来了额外的负担,但是在任何情况下都会产生这种负担,因为Java编程语言是并发的。
即使在x86下,JIT编译器的实例初始化重新排序也是可能的。 但是,编写可以触发此类重新排序的代码有点棘手。 关于如何重现这种重新排序,请参阅我的问题:
Hotspot JIT编译器是否可以重现任何指令重新排序?