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

Posted

技术标签:

【中文标题】JMM 保证 final 作为字段和对对象的非最终引用【英文标题】:JMM guarantees about final as field and non final reference to the object 【发布时间】:2017-06-16 18:11:23 【问题描述】:

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

让我们研究代码:

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));
    

我有一些问题:

    jmm 是否保证 if 应用程序终止然后输出 [1,2] ? jmm 是否保证 instance.data 在循环终止后不为空?

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 是否会在我的示例中将 final 替换为 volatile。

因此我想了解有关 4 个新病例的解释

【问题讨论】:

【参考方案1】:

是的,有一些问题。您正在循环之后重新读取instance 变量,并且由于两次读取都是活泼的,因此退出循环并不能保证循环后的读取读取非null 引用。

由于此问题不是问题的主题,因此假设以下更改:

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

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

参考JLS §17.5., final Field Semantics:

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

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

o为对象,co的构造函数,其中final字段f em> 被写入。当 c 正常或突然退出时,对 ofinal 字段 f 进行冻结操作。


如果将变量更改为volatile,则几乎没有任何保证。 volatile 字段在对该变量的写入和后续读取之间建立 happens-before 关系,但经常被忽视的关键点是单词“subsequent”。如果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 对字段的数组引用写入之前放置另一个数组写入,不会改变语义。

【讨论】:

@g***:我认为 John Vint 已经回答了这个问题。通常,您不应将对象可见性(如privateprotected)与线程可见性混淆。当另一个线程可以同时调用访问私有字段的方法时,您可能会发生数据竞争。 是的,没有很好地发现差异。希望它现在回答它。 @Eugene:我通常避免用这些障碍来解释 JMM,因为它们根本没有出现在规范中。 “JSR-133 Cookbook”对于那些计划编写编译器或优化器的人来说非常有趣,但对于应用程序开发人员来说却不是那么有趣。我很好奇 Java 9 可能带来的变化,就像它一样,显式栅栏成为 API 的一部分。由于这应该反映在规范中,它可能会影响,将来如何解释它...... @Eugene:如你所说,我等官方规范,可以参考。您可能已经注意到,当我的意思相同时,我会仔细尝试使用与规范完全相同的词,因为很多这些词在口语中使用时会严重超载,这是一个很容易造成额外混淆的主题… @Holger,我认为 NullPointerException 也是一个可能的结果。即使instance == null 检查返回false 并且循环终止,JMM 也不保证instance.data 看到对instance 的非空引用。为final 字段提供的保证是使用内存链偏序制定的:在最简单的情况下,内存链边缘出现如果读取碰巧看到写入的结果。这些内存链边缘在程序顺序方面不具有传递性,因此一次读取看到写入的结果并不能保证另一个簧片会看到相同的结果。

以上是关于JMM 保证 final 作为字段和对对象的非最终引用的主要内容,如果未能解决你的问题,请参考以下文章

Java中同步的可见性效果

[JMM]__JMM中的普通final域重排序规则

[JMM]__JMM中引用类型final域重排序规则

深入理解JMM(Java内存模型) --final

JMM简介

最终的局部变量不能分配,不能分配给非最终变量