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

Feu*_*mel 12 java concurrency

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

是否正确使用以下类定义Foo,一个线程执行Foo.initFoo();并执行不同的线程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
    }
}
Run Code Online (Sandbox Code Playgroud)

根据我对Java内存模型(以及其他语言中的内存模型)的了解,实际上我并不感到惊讶,这是可能的,但直觉投票非常强烈,因为它是不可能的(可能因为涉及对象初始化而对象初始化似乎如此在Java中神圣).

如果0没有第一个线程中的同步,是否可以"修复"此代码(即它永远不会打印)?

ass*_*ias 5

一个调用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编程语言是并发的.