Java final field 异端邪说:我的推理正确吗?

Java final field heresy: am I reasoning correctly?

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

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

final 字段的值保证对构造函数 returns 之后的任何其他线程可见。但是对对象本身的引用必须以线程安全的方式发布。 如果引用被安全发布,那么 final 的可见性保证就变得多余了。

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

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

我错了吗?

编辑: 解释 Java 并发实践中的第 3.5 节,

Two things can go wrong with improperly published objects. Other threads could see a stale value for the reference, and thus see a null reference or other older value even though a value has been set. But far worse, other threads could see an up-to-date value for the reference, but stale values for the state of the object.

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

编辑 2:这个问题是由于术语混淆引起的。

就像 a similar question 的提问者一样,我一直理解 "safe publication" 这个词的意思是对象的内部状态 对对象的引用保证对象本身对其他线程可见。为了支持这个定义,Effective Java 引用 Goetz06, 3.5.3 将 "safe publication" 定义为(强调)

Transferring such an object reference from one thread to others

同样赞成这个定义,请注意上面解释的 Java 实践中的并发性部分指的是可能过时的引用 "improperly published."

无论您怎么称呼它,我都不认为不安全地发布对不可变对象的引用会有用。但是根据this answer,是可以的。 (给出的例子是一个原始值,但同样的原则也适用于参考值。)

But the reference to the object itself must be published in a thread safe manner. If the reference is published safely, then the visibility guarantee of final becomes redundant.

第一句错误;因此,第二个无关紧要。 final 在其他安全发布技术(如同步或 volatile 存在的情况下可能是多余的。但不可变对象的要点在于它们固有 线程安全,这意味着无论引用如何发布,它们都将处于一致状态。因此,您首先不需要那些其他技术,至少就安全发布而言是这样。

编辑:OP 正确地指出术语 "safe publication" 存在一些歧义。在这种情况下,我指的是对象内部状态的一致性。在我看来,影响引用的可见性问题是一个有效但独立的问题。

我已经读过你的问题好几遍了,但仍然有一些问题低估了它。我将尝试增加另一个答案——这实际上是正确的。 Java 中的 final 所有关于重新排序的东西 (或如您所说的先发生)。

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

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

但是当前的实现并没有那样做——这意味着内存屏障不会在每次写入 final 后发生——它们发生在构造函数的末尾,请参阅this。你会看到这一行很重要:

 _exits.insert_mem_bar(Op_MemBarRelease, alloc_with_final());

Op_MemBarRelease 是实际的 LoadStore|StoreStore 障碍,因此 在当前的实现下 对于 [=38 有一个最终的字段就足够了=]所有其他人也将安全发布。但是,当然,这样做需要您自担风险。

在这种情况下,

volatile 的发布还不够——因为它根本不会引入必要的障碍,您可以多读一点 .

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