Java 实例初始化和分配到共享变量的重新排序是否可能?

Java 实例初始化和分配到共享变量的重新排序是否可能?,java,concurrency,Java,Concurrency,我正在阅读,它实际上谈到了双重检查锁定,但我对示例代码中更基本的失败感到惊讶。此处指出,实例的初始化(即在构造函数返回之前写入实例变量)可能在引用实例写入共享变量(以下示例中的静态字段)后重新排序 对于类Foo的以下定义,一个线程执行Foo.initFoo(),这是真的吗和执行System.out.println(Foo.Foo.a)的不同线程,第二个线程可以打印0(而不是1或抛出NullPointerException) 从我对Java内存模型(以及其他语言中的内存模型)的了解来看,这一点并不

我正在阅读,它实际上谈到了双重检查锁定,但我对示例代码中更基本的失败感到惊讶。此处指出,实例的初始化(即在构造函数返回之前写入实例变量)可能在引用实例写入共享变量(以下示例中的静态字段)后重新排序

对于类
Foo
的以下定义,一个线程执行
Foo.initFoo(),这是真的吗和执行
System.out.println(Foo.Foo.a)的不同线程
,第二个线程可以打印
0
(而不是
1
或抛出
NullPointerException

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


如果不在第一个线程中进行同步,是否可以“修复”此代码(即它永远不会打印
0
)?

调用
foo=new foo()
涉及多个操作,这些操作可能会被重新排序,除非引入适当的同步来防止:

  • 为新对象分配内存
  • 写入字段的默认值(
    a=0
  • 写入字段的初始值(
    a=1
  • 发布对新创建对象的引用
  • 如果没有适当的同步,步骤3和步骤4可能会被重新排序(注意,步骤2必然发生在步骤4之前),尽管在x86体系结构上hotspot不太可能发生这种情况

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

    • 使
      a
      final
    • 同步访问
      foo
      (使用同步的
      init
      和getter)

    不必深入JLS#17的复杂性,您可以阅读有关类初始化的内容(我的重点):

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


    即使在x86下,JIT编译器也可以对实例进行初始化重新排序。 然而,编写能够触发这种重新排序的代码有些棘手。 关于如何重现这种重新排序,请参见我的问题:


    您的代码似乎缺少如您所述的System.out.println()。您可以发布完整的代码吗?它还缺少对
    Foo.initFoo()
    的调用。我已经将这些语句添加为单独的静态函数,它将在不同的线程上调用,而不会在线程之间进行同步。您的回答让人松了一口气!我已经习惯了这样一个事实,即通过设置共享变量来发布不可变对象是线程安全的,而无需同步,因此我从来没有想到,由于对象的不可变性,它是唯一线程安全的@Feuermurmel有两件事你应该知道1。声明字段
    volatile
    就足以进行必要的同步,以防止重新排序所述的关联。2.一个完全不可变的对象(假设所有字段都是final)不会出现我们在这里看到的部分初始化对象的风险。@JohnVint这是否意味着声明静态字段
    Foo.Foo
    transient而不是字段
    Foo.A
    final也可以解决问题(假设没有其他地方写入Foo.a)@Feuermurmel不是瞬态的、易失的。是的,有了
    public static volatile Foo-Foo;
    ,你也有一个安全的出版物,
    a
    保证被视为1。@GaborSch哦,哇,我失败得很惨!我的意思是
    volatile
    当然!
    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
        }
    }