为什么Final字段在Java中不能挥发?

时间:2013-09-11 18:39:06

标签: java volatile

我想了解为什么声明为final的Reference不能声明为Volatile。 SO上有一个类似的问题[Why can an Object member variable not be both final and volatile in Java?

[1]:Why can an Object member variable not be both final and volatile in Java?但我不确定在答案中是否理解了FINAL。

现在最终变量的状态在初始化后肯定可以更改。只能将引用初始化为另一个对象。

例如,考虑以下成员变量

final StringBuilder sb = new StringBuilder("CAT");

现在另一个线程将sb更改为:

sb.append("S");

如果此变量是非易失性的,那么根据Java内存模型,此更改是否可用于不同的线程?

编辑:我将StringBuffer改为StringBuilder,让一些人明白我的观点。

7 个答案:

答案 0 :(得分:16)

volatile意味着该领域将发生变化。如果它是最终的,你永远不会改变它,这是没有意义的。

  

根据Java内存,此更改是否可用于不同的线程   模型,如果这个变量是非易失性的?

在发生易失性写入后,声明字段volatile对其内容或修改没有影响。如果字段是易失性的或非易失性的,则append的记忆效应由StringBuffer的实现决定。

因此,在StringBuffer的情况下,它确实可以确保内存可见性,但不是出于您的想法。 StringBuffer是同步的(线程安全的),因此您将拥有内容的始终最新值。

另一方面,

StringBuilder未同步,无法保证内存可见性。因此,如果您尝试将两者交换为多线程测试,您会看到不同的结果。

答案 1 :(得分:9)

因为volatile会影响线程访问和编辑变量的行为。在您的代码示例中,变量是对象的引用(sb)。因此,这意味着volatile对象具有效果。 final锁定引用。因此,如果您附加文本,则不会更改引用。因此,同时使用volatilefinal毫无意义。

  

如果此变量是非易失性的,那么根据Java内存模型,此更改是否可用于不同的线程?

因为您使用StringBuilder使用非线程安全实现,所以无法保证所有线程都具有StringBuilder的最新状态。

答案 2 :(得分:5)

  

如果此变量为非易失性

,则根据Java内存模型,此更改是否可用于不同的线程

是的,但那是因为StringBuffer是线程安全的 - 这意味着它在内部提供锁定,这将导致内存障碍,以便其他线程看到更新。

作为volatile的引用不会影响对象的操作,它会影响引用。

所以你可以做到

 volatile StringBuilder b = new StringBuilder();

 b = someOtherStringBuilder;

现在由于b是易失性的,其他线程会看到引用的更新。

无论如何做

 b.append("foo");

无法保证其他线程会看到对现有b对象的更改。与StringBuffer不同,StringBuilder不是线程安全的,所以如果不提供自己的锁定,你不应该这样做。

如果您希望保证b.append("foo");在没有任何锁定的情况下对其他线程可见,则StringBuilder中的每个成员字段也需要是volatile。 (虽然这不会使线程安全)

答案 3 :(得分:4)

请记住,volatilefinal会影响变量/成员,但不会影响名为/引用的对象的可变性(或操作的线程安全性)其中! (其他答案解释了为什么修饰语在一起没有意义。)

在给定的情况下,sb.append(..) 修改命名/引用的对象(虽然它不修改变量/成员)并且不安全 用于使用任一修饰符的交叉线程。


正如其他人所指出的那样,StringBuilder添加了一些内部同步,这个仅暗示它不会在其内部数据结构级别“被破坏”。

但是,切换到StringBuilder仍然 [固有]线程安全。线程安全已经很多与各种操作的原子范围有关 - 并且在没有看到所有的情况下进行判断以及如何允许它在有效的exectuion会产生误导。

例如,考虑从多个线程执行以下代码(并假设sb现在是StringBuilder对象)。订单会是什么?

sb.append("a").append("b");

如果代码更改如下,会发生什么?哪个[更多]是“线程安全的”?

synchronized (this) {
  sb.append("a").append("b");
}

答案 4 :(得分:3)

这个问题完全误解了跨多个线程的对象引用的含义。一般来说,volatile在原语周围最有趣,而且通常是你看到它的地方。

Final和volatile仅影响变量,因为它包含一个对象引用,它是Object在内存中的位置的句柄,仅此而已。因此,为了说明,我们假设对StringBuilder(或其他)的引用是9AF5。如果这是最终的,则在构造对象后它不会改变。因此(至少从Java 1.5开始,之前就不记得了)线程安全,因为任何线程都可以引用该变量,引用它的副本,或者CPU想要做的关于优化该访问的任何内容,因为最后当天,它不会改变,因此如果你看9AF5,你不必检查最终声明对象中包含的引用的值是否实际改变了。

由于final确保线程安全性达到volatile所能达到的水平,因此volatile是多余的。

如果变量是非final的,那么volatile确保所有线程一致地看到9AF5,并且如果你改变了值(例如通过分配一个新的StringBuilder),那么现在该值变为1D7C,没有线程将是持有无效的旧副本并认为该值仍为9AF5。

这些都不会影响StringBuilder对象的内容,只会影响它在内存中的位置。

答案 5 :(得分:2)

volatilefinal提供的内存可见性保证实际上非常相似:

  • volatile保证在写入volatile变量之前线程A所做的任何更改都将在后续从该变量读取后被线程B看到

  • final保证线程A在初始化该字段之前对final字段引用的对象的状态所做的任何更改都将由线程B看到,如果线程B访问该字段通过该领域的对象

正如您所看到的,鉴于final字段只能正常初始化一次,这些保证非常相似 - 唯一的主要区别是final字段提供的保证仅涵盖引用的对象在那个领域。

因此,在同一领域结合这些保证没有多大意义。

当所引用对象的状态发生变化而不更改字段本身时,两种保证都不会涵盖相关情况。

答案 6 :(得分:0)

当你将对象字段声明为final时,你需要在object的构造函数中初始化它,然后final字段不会改变它的值。 当你将对象的字段声明为volatile时,字段的值可以改变,但是任何线程的每次读取值都会看到写入的最新值。

因此,一个volatile字段可以保证您更改时会发生什么。 (没有可能是参考的对象)。最终字段不可能出现(可以更改字段引用的内容)。两者都没有意义。