Java final field heresy:我的推理是正确的吗?

时间:2017-08-29 00:13:20

标签: java c# thread-safety final

我最近开始学习C#并直接学习内存模型。 C#和Java在volatile字段的读写方面有类似(尽管可能不相同)的线程安全保证。但与Java中对final字段的写入不同,对C#中的readonly字段的写入不提供任何特定的线程安全保证。考虑线程安全如何在C#中工作,让我怀疑final字段在Java中的行为方式是否有任何真正的优势。

三年前我了解了final的重要性。我问this question并得到了我接受的详细答案。但现在我认为这是错误的,或者至少是无关紧要的。我仍然认为字段应尽可能final,而不是出于普遍认为的原因。

在构造函数返回后,final字段的值保证对任何其他线程可见。但是对象本身的引用必须以线程安全的方式发布。 如果安全发布参考,则final的可见性保证会变得多余。

我考虑过它与public static字段有关的可能性。但从逻辑上讲,类加载器必须同步类的初始化。同步使final的线程安全性变得多余。

所以我提出了一个异端的观点,即final的唯一真正价值在于使不变性自我记录和自我实施。在实践中,私有非final字段(,特别是数组元素)是完全线程安全的,只要它们在构造函数返回后不被修改。

我错了吗?

编辑:释义Java Concurrency in Practice中的第3.5节

  

不正确发布的对象可能会出现两件事。其他线程可以看到引用的陈旧值,因此即使已设置值,也会看到空引用或其他旧值。但更糟糕的是,其他线程可以看到参考的最新值,但是对象状态的陈旧值。

我理解final字段如何解决第二个问题,但不是第一个问题。到目前为止,最高投票的答案认为第一个问题不是问题。

编辑2:这个问题源于术语的混淆。

就像a similar question的提问者一样,我总是理解“安全出版物”这个术语"这意味着对象的内部状态对对象本身的引用都保证对其他线程可见。为了有利于这个定义,Effective Java引用了Goetz06,3.5.3来定义"安全发布" as(强调补充)

  

将这样的对象引用从一个线程传输到其他线程

同样赞成这个定义,请注意上面的Java Concurrency in Practice一节中提到的可能过时的引用被称为"不正确地发布。"

无论你怎么称呼它,我都不认为不安全地发布对不可变对象的引用可能是有用的。但根据this answer,它可以。 (给出的示例是原始值,但相同的原则可以应用于参考值。)

2 个答案:

答案 0 :(得分:4)

  

但是对象本身的引用必须以线程安全的方式发布。 如果安全发布参考,则final的可见性保证会变得多余。

第一句话错了;因此,第二个是无关紧要的。在存在其他安全发布技术(如同步或final)时,volatile可能是多余的。但是,不可变对象的意义在于它们本身 线程安全,这意味着无论引用如何发布,它们都将以一致的状态被看到。因此,您首先不需要其他技术,至少就安全出版而言。

编辑:OP正确指出围绕术语"安全发布"存在一些含糊之处。在这种情况下,我指的是对象内部状态的一致性。在我看来,影响参考的可见性问题是一个有效但独立的问题。

答案 1 :(得分:0)

我已经多次阅读过您的问题但仍有一些问题低估了它。我将尝试增加另一个答案 - 这实际上是正确的。 Java中的final 所有关于重新排序(或者在您调用它之前发生)。

首先,这由JLS保证,请参阅Final Field Semantics。在该示例中,您可以看到其他线程可以保证正确看到单个final字段,而另一个则不能。 JLS是正确的,但在当前的实现下,单个字段为final就足够了 - 进一步阅读。

对构造函数中的最后一个字段的每次写入后跟两个memory barriers - StoreStoreLoadStore(因此发生之前的名称);因为最终字段的存储将在读取到同一字段之前发生 - 通过内存屏障保证。

但是当前的实现没有这样做 - 意味着在每次写入最终的之后都不会发生内存障碍 - 它们发生在构造函数的末尾,请参阅this 。你会看到这一行非常重要:

 _exits.insert_mem_bar(Op_MemBarRelease, alloc_with_final());

Op_MemBarRelease是实际LoadStore|StoreStore屏障,因此在当前实施下,只需要一个字段为所有其他字段安全也发表了。但是,当然,这样做需要您自担风险。

在这种情况下,

volatile不会使出版物足够 - 因为它不会引入必要的障碍,你可以多读一点here

请注意,引用发布不是问题,只是因为JLS说:Writes to and reads of references are always atomic, regardless of whether they are implemented as 32-bit or 64-bit values.