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

2022-09-04 22:10:10

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

在类的以下定义中,当一个线程执行而另一个线程执行时,第二个线程可以打印(而不是或抛出一个)这是真的吗?FooFoo.initFoo();System.out.println(Foo.foo.a);01NullPointerException

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


答案 1

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

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

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

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

  • 最终定稿a
  • 同步访问(使用同步的 AND getter)。fooinit

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

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


答案 2

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

热点JIT编译器是否完成了任何可以复制的指令重新排序?


推荐