不久前,我编写了一个Java servlet过滤器,该过滤器在其init
函数中加载配置(基于web.xml
的参数)。 筛选器的配置缓存在私有字段中。 我在字段上设置了volatile修饰符。
后来,当我检查Sonar公司以查看是否在代码中发现任何警告或问题时,得知使用volatile违反了规定,我感到有些惊讶。 解释为:
通常使用关键字“ volatile”来微调Java应用程序,因此需要Java内存模型的专业知识。 而且,它的作用范围还不太清楚。 因此,volatile关键字不应用于维护目的和可移植性。
我同意volatile是许多Java程序员所不知道的。 对于一些甚至未知。 不仅因为它从一开始就没有使用太多,还因为它的定义自Java 1.5起就发生了变化。
让我稍微回顾一下Sonar的违规行为,首先解释volatile在Java 1.5及更高版本中的含义(直到撰写本文时Java 1.8)。
什么是挥发物?
尽管volatile修饰符本身来自C,但在Java中它具有完全不同的含义。 这可能无助于加深对它的理解,使用谷歌搜索挥发物可能会导致不同的结果。 让我们快速迈出第一步,看看volatile在C语言中的含义。
在C语言中,编译器通常假定变量无法自行更改值。 尽管这是默认行为,但有时变量可能表示可以更改的位置(例如硬件寄存器)。 使用易失性变量指示编译器不要应用这些优化。
回到Java。 C中的volatile的含义在Java中将毫无用处。 JVM使用本机库与操作系统和硬件进行交互。 此外,将Java变量指向特定地址根本是不可能的,因此变量实际上不会自行更改值。
但是,JVM上变量的值可以由不同的线程更改。 默认情况下,编译器假定变量不会在其他线程中更改。 因此,它可以应用优化,例如对存储器操作重新排序以及将变量缓存在CPU寄存器中。 使用易失性变量指示编译器不要应用这些优化。 这样可以保证读取线程始终从内存(或共享缓存)中读取变量,而不从本地缓存中读取变量。
原子性
更进一步,关于32位JVM的volatile使写入到64位可变原子(如long
或double
)成为可能。 要写入变量,JVM会指示CPU将操作数写入内存中的某个位置。 使用32位指令集时,如果变量的大小为64位怎么办? 显然,该变量必须用两条指令(一次32位)写入。
在多线程方案中,另一个线程可能会在写入过程中读取变量。 此时,仅写入变量的前半部分。 这种争用条件可以通过可变的方式来防止,从而有效地使对32位体系结构的64位变量进行原子写入。
请注意,上面我谈到的是写而不是更新 。 使用volatile不会使更新原子化。 例如,当i
易失时, ++i
将从堆或L3缓存中读取i
的值到本地寄存器inc
中,然后将该寄存器写回到i
的共享位置。 在读写之间, i
可能会被另一个线程更改。 在读写指令周围加一个锁,使更新成为原子操作。 或更妙的是,使用concurrent.atomic
包中原子变量类的非阻塞指令。
副作用
volatile变量在内存可见性方面也有副作用。 当线程读取volatile变量时,不仅对volatile变量的更改对其他线程可见,而且导致更改的代码的任何副作用也可见。 或更正式地说,易失性变量与该变量的后续读取之间建立事前关联。
即,从内存可见性的角度来看,有效地写入易失性变量就像退出同步块并读取易失性变量就像进入变量一样。
选择易失性
回到我使用volatile一次初始化配置并将其缓存在私有字段中的情况。
到目前为止,我相信确保此字段对所有线程可见的最佳方法是使用volatile。 我本来可以使用AtomicReference
。 由于该字段仅被写入一次(在构造之后,因此不可能是最终的),因此原子变量传达了错误的意图。 我不想使更新原子化,我想使缓存对所有线程可见。 对于它的价值,原子类也使用volatile。
关于声纳法则的思考
既然我们已经了解了volatile在Java中的含义,那么让我们进一步讨论一下Sonar规则。
在我看来,此规则是Sonar之类的工具配置中的缺陷之一。 如果您需要跨线程共享(可变)状态,那么使用volatile可能是一件非常好的事情。 当然,您必须将此保持在最低水平。 但是,此规则的结果是,不了解什么是挥发性的人会遵循建议不要使用挥发性。 如果他们有效地删除修饰符,则会引入竞争条件。
我确实认为使用未知或危险的语言功能时自动升起红色标记是个好主意。 但是,也许只有当有更好的替代方案来解决同一问题时,这才是一个好主意。 在这种情况下,volatile没有其他选择。
请注意,这绝不是对Sonar的指责。 但是,我确实认为人们应该选择一套他们认为重要的规则来应用,而不是采用默认配置。 我发现使用默认情况下启用的规则的想法有点天真。 您的项目很有可能不是工具维护者选择标准配置时考虑的项目。
此外,我相信当您遇到未知的语言功能时,您应该了解它。 当您了解它时,可以决定是否有更好的选择。
实践中的Java并发
关于JVM中并发性的事实上的标准书是Brain Goetz编写的Java Concurrency in Practice 。 它从多个详细级别解释了并发的各个方面。 如果您在Java(或不纯的Scala)中使用任何形式的并发,请确保至少阅读本书的前三章,以对问题有一个较高的了解。
翻译自: https://www.javacodegeeks.com/2014/08/javas-volatile-modifier-2.html