初始化非最终字段

2022-09-04 03:50:53

我目前正在阅读JSR-133(Java内存模型),我不明白为什么f.y可能未初始化(可以看到0)。有人可以向我解释一下吗?

class FinalFieldExample {
    final int x;
    int y;
    static FinalFieldExample f;

    public FinalFieldExample() {
        x = 3;
        y = 4;
    }

    static void writer() {
        f = new FinalFieldExample();
    }

    static void reader() {
        if (f != null) {
            int i = f.x; // guaranteed to see 3
            int j = f.y; // could see 0
        }
    }
}

答案 1

这被称为“过早发布”效应。

简单来说,允许JVM对程序指令进行重新排序(出于性能原因),如果这种重新排序不违反JMM的限制。

您希望代码像这样运行:f = new FinalFieldExample();

1. 创建 2 的
实例。将 3 分配给 x
3。将 4 分配给 y
4。将创建的对象分配给变量FinalFieldExamplef

但是在提供的代码中,没有什么可以阻止JVM进行指令重新排序,因此它可以运行如下代码:

1. 创建 2 的
实例。将 3 分配给 x
3。将原始的、未完全初始化的对象分配给变量
4。将 4 分配给 yFinalFieldExamplef

如果在单线程环境中发生重新排序,我们甚至不会注意到它。这是因为我们期望,在我们开始使用它们之前,对象将被完全创建,并且JVM尊重我们的期望。现在,如果多个线程同时运行此代码,会发生什么情况?在下一个示例中,Thread1 正在执行方法和 Thread2 - 方法:writer()reader()

线程 1:创建线程 1 的
实例:将 3 分配给 x
线程 1:将原始对象,未完全初始化的对象分配给变量
线程 2:读取,它不是 null
线程 2:读取 f.x,它是 3
线程 2:读取 f.y,它仍然是 0
线程 1:将 4 分配给 yFinalFieldExampleff

绝对不好。为了防止JVM这样做,我们需要给它提供有关程序的其他信息。对于此特定示例,有一些方法可以修复内存一致性:

  • 声明为变量。这将导致“冻结”效应。简而言之,如果在构造过程中未泄漏对对象的引用,则最终变量将始终在您访问它们的那一刻初始化。yfinal
  • 声明为变量。这将创建“同步顺序”并解决问题。简而言之,指令不能在易失性写入和易失性读取之上重新排序。赋值给变量是一种易失性写入,这意味着指令在赋值后无法重新排序和执行。从变量读取是一种易失性读取,因此读取不能在它之前执行。v 写入和 v 读取的组合称为同步顺序,可提供所需的内存一致性。fvolatilefnew FinalFieldExample()ff.x

这是一个很好的博客,可以回答您关于JMM的所有问题。


答案 2

JVM 可以对内存的读取和写入进行重新排序,因此对 的引用可以写入主内存之前的值。如果另一个线程在这两个写入之间读取,它将读取 。但是,如果通过写入 或 字段来创建内存屏障,则屏障之后的读取和写入不能相对于屏障之前的读取和写入重新排序。因此,可以保证在另一个线程读取之前编写两者。ff.yf.y0finalvolatileff.yf

我在这里问了一个类似的问题。答案会更加详细。