Java 内存模型中的最终字段

final Fields In The Java Memory Model

您能解释一下 f.y 的值如何被视为 0 而不是 4 吗? 那是因为其他线程写入将值从 4 更新为 0 吗? 这个例子取自jls https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.5

 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
            } 
        } 
    }

假设我们启动了两个线程,像这样:

new Thread(FinalFieldExample::writer).start(); // Thread #1
new Thread(FinalFieldExample::reader).start(); // Thread #2

我们可能会观察到程序的实际操作顺序如下:

  1. Thread #1 写入 x = 3.
  2. Thread #1 写入 f = ....
  3. Thread #2读取f发现不是null.
  4. Thread #2 阅读 f.x 并看到 3
  5. Thread #2f.y看到0,因为y好像还没有写
  6. Thread #1 写入 y = 4.

换句话说,Threads #1#2 能够让它们的操作交错,使得 Thread #2Thread #1 写入之前读取 f.y它。

另请注意,允许对 static 字段 f 的写入进行重新排序,以便它看起来发生在写入 f.y 之前。这只是缺乏任何同步的另一个结果。如果我们将 f 也声明为 volatile,将阻止这种重新排序。


评论中有一些关于使用反射写入 final 字段的讨论,这是事实。这在 §17.5.3:

中讨论

In some cases, such as deserialization, the system will need to change the final fields of an object after construction. final fields can be changed via reflection and other implementation-dependent means.

因此在一般情况下 Thread #2 可以在读取 f.x 时看到任何值。

还有一种更传统的方式来查看 final 字段的默认值,只需在赋值前泄漏 this

class Example {
    final int x;

    Example() {
        leak(this);
        x = 5;
    }

    static void leak(Example e) { System.out.println(e.x); }

    public static void main(String[] args) { new Example(); }
}

我认为如果FinalFieldExample的构造函数是这样的:

static FinalFieldExample f;

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

Thread #2 也可以将 f.x 读作 0

本文来自§17.5:

An object is considered to be completely initialized when its constructor finishes. A thread that can only see a reference to an object after that object has been completely initialized is guaranteed to see the correctly initialized values for that object's final fields.

final 规范的更多技术部分也包含类似的措辞。

Could you explain how the value of f.y could be seen 0 instead of 4?

在 Java 中,compiler/JVM 执行的一项重要优化是指令的重新排序。只要不违反语言规范,编译器可以出于效率原因自由地重新排序所有指令。在对象构建过程中,有可能实例化一个对象,完成构造函数,并在发布其引用之前对象中的所有字段都已正确初始化。

但是,Java 语言表示,如果一个字段被标记为 final,那么它必须在构造函数完成时正确初始化。引用自 Java language specs you reference 的部分。重点是我的。

An object is considered to be completely initialized when its constructor finishes. A thread that can only see a reference to an object after that object has been completely initialized is guaranteed to see the correctly initialized values for that object's final fields.

因此,当 FinalFieldExample 被构造并分配给 f 时,x 字段 必须 正确初始化为 3 但是y 字段可能已正确初始化,也可能未正确初始化。因此,如果线程 1 调用 writer(),然后线程 2 调用 reader() 并认为 f 不为空,则 y 可能为 0(尚未初始化)或 4 (已初始化)。