JLS中,为什么final字段的可见性规定前后不一致?

JLS 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字段的值是对的,引用的赋值必须得在这个对象完全初始化后来做。

也就是说,不管是final还是非final字段,在这个这个引用赋值之前就可见了。

但是,JLS紧接着一个例子:

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
        } 
    } 
}
The class FinalFieldExample has a final int field x and a non-final int field y. One thread might execute the method writer and another might execute the method reader.

也就是说,只保证final字段可见,而不保证非final字段了。

请问,这两点如何理解?是JLS前后规定不一致吗?

阅读 3.4k
3 个回答
即为了保证final字段的值是对的,引用的赋值必须得在这个对象完全初始化后来做。

也就是说,不管是final还是非final字段,在这个这个引用赋值之前就可见了

这个推论是不成立的。 JLS 一定要把 final 字段单独拿出来说,就是因为非 final 字段是没有这个性质的。上面的引用仅讨论了 final 字段,与非 final 字段无关。


即使是完全初始化之后,对非 final 字段,在另一个线程,可能也看不到“正确”的初始化值。

JMM内存屏障,final域的写入和构造函数的执行可以保证不会重排,也就是说构造函数执行完毕后final域必然会被初始化,所以线程reader可以读到正确的值。

新手上路,请多包涵

内存重排序会导致对象引用被发布之后,对象去还未被完全的初始化:
知识点:重排序定义三种形式的不可重排情况(对于同一种变量):
写后读
写后写
读后写

案例:问题中的对象构造并被发布的过程:
A 写x值 写
B 写y值 写
C 发布引用 写
D 使用引用 读
E 使用x值 读
F 使用y值 读

对于上述过程,根据重排序规则可以看出
A先于E
B先于F
C先于D
但是ABC之间没有依赖的关系,所以是可以被内存重排序的

重点就在于这里:
除了final修饰的内容,其他的操作是可以重排的,只要满足重排序的规则即可,所以会存在这样的情况:
A 写x值 写
C 发布引用 写
B 写y值 写
D 使用引用 读
E 使用x值 读
F 使用y值 读

多线程情况下,当C步骤完成了,线程发生切换,另外一个线程可能就会拿到一个未完全初始化的对象

A不会重排,是因为final的内存语义规定了:
1> 在构造函数内对一个final域的写入,与随后把这个构造函数的引用赋值给一个引用变量,两个操作不能重排序
2> 初次读一个包含final域对象的引用,和随后初次读这个final域,这两个操作不能重排序

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题