2

你能解释一下 fy 的值是怎么看成 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
            } 
        } 
    }
4

2 回答 2

5

假设我们启动了两个线程,如下所示:

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 #2阅读f.y和看到0,因为y似乎还没有被写入。
  6. Thread #1写道y = 4

换句话说,Threads #1并且#2能够以一种先读后Thread #2写的方式交错操作。f.yThread #1

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


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

在某些情况下,例如反序列化,系统将需要final在构造后更改对象的字段。final可以通过反射和其他依赖于实现的方式更改字段。

因此,在一般情况下,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(); }
}

我认为 ifFinalFieldExample的构造函数是这样的:

static FinalFieldExample f;

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

Thread #2f.x也可以阅读0

这是来自§17.5

当一个对象的构造函数完成时,它被认为是完全初始化的。只有在对象完全初始化后才能看到对该对象的引用的线程可以保证看到该对象final字段的正确初始化值。

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

于 2018-07-16T16:34:49.457 回答
1

你能解释一下如何f.y看到 0 而不是 4 的值吗?

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

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

当一个对象的构造函数完成时,它被认为是完全初始化的。只能在对象完全初始化后才能看到对该对象的引用的线程可以保证看到该对象的最终字段的正确初始化值

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

于 2018-07-18T17:58:25.557 回答