JMM保证最终为对象的字段和非最终引用

时间:2017-01-31 10:34:43

标签: java concurrency visibility final java-memory-model

我试着理解最终字段的语义。

让研究代码:

public class App {

    final int[] data;
    static App instance;

    public App() {
        this.data = new int[]{1, 0};
        this.data[1] = 2;
    }


    public static void main(String[] args) {
        new Thread(new Runnable() {
            public void run() {
                instance = new App();
            }
        }).start();

        while (instance == null) {/*NOP*/}
        System.out.println(Arrays.toString(instance.data));
    }
}

我有一些问题:

  1. jmm是否保证如果应用程序终止则输出[1,2]?
  2. 在循环终止后,jmm是否保证instance.data不为空?
  3. P.S。我不知道如何使标题正确,随时可以编辑。

    其他

    如果我们更换,是否存在可见性差异:

    public App() {
        this.data = new int[]{1, 0};
        this.data[1] = 2;
    }
    

    public App() {
        int [] data = new int[]{1, 0};
        data[1] = 2;
        this.data = data;    
    }
    

    我也想知道wjat将在我的例子中用volatile替换final

    因此,我希望得到关于4个新案例的解释

1 个答案:

答案 0 :(得分:6)

是的,如果应用程序终止,它将输出[1,2]。关键是final字段语义作为一个整体应用于构造函数,将数组引用写入字段时的确切时间是无关紧要的。这也意味着在构造函数中,重新排序是可能的,因此如果this引用在构造函数完成之前转义,则所有保证都是无效的,无论this是否在程序中的写入之前或之后转义订购。因为在你的代码中,this在构造函数完成之前没有转义,所以保证适用。

请参阅JLS §17.5., final Field Semantics

  

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

请注意,它指的是完全初始化的状态,而不是对特定final字段的写入。这也将在下一节§17.5.1中解决:

  

o 成为对象, c o 的构造函数,其中final字段 f 写的。当 c 正常或突然退出时,会对 o final字段 f 执行冻结操作。

如果您将变量更改为volatile,则几乎不会有任何保证。 volatile字段在对该变量的写入与后续读取之间建立发生之前关系,但经常被忽略的关键点是“后续” 。如果App实例未正确发布,就像在您的示例中一样,则不保证主线程对instance.data的读取将是后续的。如果它读取现在可能的null引用,那么您知道它不是后续的。如果它读取非null引用,您知道它是在字段写入之后,这意味着您可以保证在第一个插槽中读取1,但是对于第二个插槽,您可能会读到02

如果您想根据障碍和重新排序来讨论这个问题,volatile写入data会保证所有先前的写入都已提交,其中包括将1写入第一个数组槽,但不保证后续的非volatile写入不会提前提交。因此,App引用的不正确发布仍然可能在volatile写入之前执行(尽管很少发生)。

如果将写入移动到构造函数的末尾,则在看到非null数组引用后,所有先前的写入都是可见的。对于final字段,它不需要进一步讨论,如上所述,写入在构造函数中的实际位置无论如何都是无关紧要的。对于volatile情况,如上所述,您不能保证读取非null引用,但是当您读取它时,所有先前的写入都将被提交。知道表达式new int[]{1, 0};无论如何都被编译为等效的hiddenVariable=new int[2]; hiddenVariable[0]=1; hiddenVariable[1]=0;可能会有所帮助。在构造之后但在volatile写入对字段的数组引用之前放置另一个数组,不会改变语义。