为什么Java中的对象成员变量不能同时是final和volatile?

52

如果我在一个类中有一个ConcurrentHashMap实例,它将被多个线程修改和读取,那么我可能会这样定义:

public class My Class {

    private volatile ConcurrentHashMap<String,String> myMap = new ConcurrentHashMap<String,String>();
...
}

final添加到myMap字段会导致错误,指出我只能使用finalvolatile。为什么它不能两者都是?


有点相关的内容:https://dev59.com/4E7Sa4cB1Zd3GeqP1THq - Jayan
8个回答

45

volatile只影响变量本身的修改,而不影响其所引用的对象。声明一个final volatile字段是没有意义的,因为final字段不能被修改。只需将字段声明为final即可。


1
只想澄清一下你的评论,即“final字段无法修改”;事实上,final字段是可变的,但是final关键字只允许赋值发生一次。 - johntrepreneur
5
@johnterpreneur说的不正确,final字段只能在对象构造期间分配,这基本上是“不可变”的定义。 - Michael Borgwardt
3
@johntrepreneur:啊,现在我知道误解出现的地方了。正如我在答案中所写的,字段和它所指的对象之间有区别——这是一个重要的区别,显然你没有理解(或是使用了错误的术语)。final表示字段本身不能被修改,但它仍然可以引用可变对象。字段的内容只是一个引用(对于非基本类型),而不是一个对象。 - Michael Borgwardt
1
@corsiKa,这就是为什么对象在构造过程中不应该泄漏的原因。这个问题并不局限于final字段... - Holger
2
@shmosel Java标准没有定义内存屏障。对于finalvolatile变量,读取和写入该值之间存在*先行发生(happens-before)*关系。通过对同一值进行后续读取,线程不会获得额外的保证。像HotSpot JVM这样的实现可能会在每次读取volatile变量时保守地插入屏障,但您仍然无法依赖此构建正确的程序,当该值未更改时。因此,对于不再更改的final变量,使用volatile就没有任何意义了。 - Holger
显示剩余9条评论

39
这是因为Java内存模型(JMM)。当你将对象字段声明为final时,你需要在对象的构造函数中初始化它,然后final字段不会改变其值。而JMM保证,在构造函数完成后,任何线程都将看到相同(正确)的final字段值。因此,你不需要使用显式同步,如synchronizeLock,以允许所有线程看到final字段的正确值。
当你将对象字段声明为volatile时,字段的值可以改变,但仍然每个线程从中读取的值将看到最新写入的值。
因此,finalvolatile实现了相同的目的——对象字段值的可见性,但第一个专门用于只能分配一次的变量,而第二个用于可以多次更改的变量。

引用:


解释对我来说很好,但我不会说final用于“常量”值。这个说法有点误导,尽管我理解你想说什么。 - Łukasz Rzeszotarski
3
“final”和“volatile”虽然实现的目的相同,但这种说法恰恰是本末倒置。 “final”的目的是声明变量或字段不能被重新赋值。而“volatile”的目的是告诉编译器该值不能通过检查代码来推断。尽管Java内存模型对这两种变量的可见性有特殊规定,但这并不是它们不重叠目的的次要原因。 - Solomon Slow
这是唯一一个实际回答核心问题的答案,“JMM承诺在ctor完成后,任何线程都将看到最终字段的相同(正确)值。”但提供的文档参考并未支持此说法。 - minsk
但是如果我们有“final int[] array”呢? 线程A分配array[index] = 42,线程B无法保证会看到这个值。 - Nick Nick

9

由于在Java中volatilefinal是两个极端,需要解释一下它们的含义。

volatile表示变量可能会被修改。

final表示无论如何变量的值都不会改变。


当您在一个线程上创建容器对象(将其最终引用写入,是的,只写一次,但在某个线程上),然后从另一个线程读取该引用时,问题就出现了。 - minsk

4

volatile关键字用于那些变量在某些情况下其值可能会发生改变,否则就不需要使用volatile。而final关键字表示该变量的值不可更改,因此不需要使用volatile

虽然并发问题很重要,但将HashMap设为volatile并不能解决问题。为了处理并发问题,您已经使用了ConcurrentHashMap


1
你不能将HashMap(或任何其他对象)设置为“volatile”。 “volatile”关键字影响变量,而不是变量所指向的对象。 - Solomon Slow

3
< p > volatile修饰符保证所有的读写操作直接进入主内存,这就像变量访问几乎是在synchronized块中完成的一样。对于不能被改变的final变量来说,这是不相关的。


3

volatile字段可以保证在改变它时发生的事情(不针对可能是它引用的对象)。

final字段不能被更改(它引用的字段可以被更改)。

同时使用两者没有意义。


这个问题的出现是因为你在一个线程上构造对象,这是引用最初设置的地方,然后可能在另一个线程上读取该引用。 - minsk

2
因为这没有任何意义。volatile影响的是对象引用值,而不是对象的字段等内容。
在你的情况下(你有并发映射),你应该将字段设置为final。

0
在多线程环境中,不同的线程将从主存中读取一个变量并将其添加到CPU缓存中。这可能导致两个不同的线程对同一变量进行更改,而忽略彼此的结果。 enter image description here 我们使用单词volatile来指示变量将保存在主存中并将从主存中读取。因此,每当线程想要读取/写入变量时,它将从主存中完成,从而在多线程环境中使变量安全。
当我们使用final关键字时,我们表明变量不会更改。正如您所看到的,如果变量是不可更改的,则无论多少个线程使用它都无所谓。没有线程可以更改变量,因此即使变量在不同时期保存到CPU缓存中,并且线程将在不同时期使用此变量,仍然可以,因为只能读取变量。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接