读写64位值时确保原子性
注意:这里说的是确保 读取 和 写入 这两个动作是原子的,而不是说关于其复合操作(加减乘除、位运算等)是原子的
出于 Java 编程语言内存模型的目的,(允许 Java 虚拟机)对于非 volatile 修饰的 long 或 double 值的单次写入被视为 2 次单独的写入:分别写入前后 32 位。这可能导致线程从一次写入中看到 64 位值的前 32 位,而从另一次写入中看到后 32 位的情况
一些(Java 虚拟机)实现可能会发现将对 64 位 long 或 double 值的单个写操作划分为对相邻 32 位值的两个写操作很方便。为了提高效率,此行为是特定于实现的;Java 虚拟机的实现可以自由执行原子和两部分的写入 long 和 double 值
两部分的写入 long 和 double 值可能导致在需要线程安全的代码中读取不确定的值。因此,多线程程序在读取或写入 64 位值时必须确保原子性
鼓励 Java 虚拟机的实现避免在可能的情况下拆分 64 位值;鼓励程序员将共享的 64 位值声明为 volatile 或正确同步其程序,以避免可能的复杂性
相关链接:
- The Java Language Specification, Java SE 8 Edition - 17.7. Non-Atomic Treatment of double and long
- 64位OpenJDK 7/8中并发长写的值完整性保证
- VNA05-J. Ensure atomicity when reading and writing 64-bit values
可见性
在一个多线程的应用中,线程在操作非 volatile 变量时,出于性能考虑,每个线程可能会将变量从主存拷贝到 CPU 缓存中。如果你的计算机有多个 CPU,每个线程可能会在不同的 CPU 中运行。这意味着,每个线程都有可能会把变量拷贝到各自 CPU 的缓存中。
出现这个问题是因为 Java 尝试尽可能地提高执行效率,缓存的主要目的是避免从主内存中读取数据。当并发时,有时不清楚 Java 什么时候应该将值从主内存刷新到本地缓存 — 而这个问题称为 缓存一致性 ( cache coherence )

每个线程都可以在处理器缓存中存储变量的本地副本。将字段定义为 volatile 可以防止这些编译器优化,这样读写就可以直接进入内存,而不会被缓存。一旦该字段发生写操作,所有任务的读操作都将看到更改。如果一个 volatile 字段刚好存储在本地缓存,则会立即将其写入主内存,并且该字段的任何读取都始终发生在主内存中
volatile 应该在何时适用于变量:
- 该变量同时被多个任务访问
- 这些访问中至少有一个是写操作
- 你尝试避免同步(在现代 Java 中,你可以使用高级工具来避免进行同步,例如
java.util.concurrent.atomic库)
重要的是要理解原子性和可见性是两个不同的概念,在非 volatile 变量上的原子复合操作是不能保证将其刷新到主内存的
同步也会让主内存刷新,所以如果一个变量由 synchronized 的方法或代码段(或者 java.util.concurrent.atomic 库里类型之一)所保护,则不需要让变量用 volatile
重排与 Happen-Before 原则
只要结果不会改变程序表现,Java 可以通过重排指令来优化性能。然而,重排可能会影响本地处理器缓存与主内存交互的方式,从而产生细微的程序 bug 。volatile 关键字可以阻止重排 volatile 变量周围的读写指令。这种重排规则称为 happens before 担保原则
happens-before 原则保证在 volatile 变量读写之前发生的指令先于它们的读写之前发生;同样,任何跟随 volatile 变量之后读写的操作都保证发生在它们的读写之后,例如:
// lowlevel/ReOrdering.java
public class ReOrdering implements Runnable {
int one, two, three, four, five, six;
volatile int volaTile;
@Override
public void run() {
one = 1;
two = 2;
three = 3;
volaTile = 92;
int x = four;
int y = five;
int z = six;
}
}
例子中 one,two,three 变量赋值操作可以被重排,但它们都发生在 volatile 变量写操作之前。同样,只要 volatile 变量写操作发生在所有语句之前, x,y,z 语句也可以被重排。这种 volatile(易变性)操作通常称为 memory barrier(内存屏障)。happens before 担保原则确保 volatile 变量的读写指令不能跨过内存屏障进行重排
happens before 担保原则还有另一个作用:当线程向一个 volatile 变量写入时,在线程写入之前的其他所有变量(包括非 volatile 变量)也会刷新到主内存。当线程读取一个 volatile 变量时,它也会读取其他所有变量(包括非 volatile 变量)与 volatile 变量一起刷新到主内存。尽管这是一个重要的特性,它解决了 Java 5 版本之前出现的一些非常狡猾的 bug ,但是你不应该依赖这项特性来“自动”使周围的变量变得易变性(volatile)
小结
- 针对
long和double这种 64 位值的读写,volatile能确保其原子性 volatile保证可见性,所有读取直接从主存读取,所有写入直接写入主存中volatile确保指令重排序时不会把其后面的指令排到volatile变量之前,也不会把前面的指令排到volatile变量之后volatile不能保证变量的复合操作是原子的,可以通过synchronized或java.util.concurrent.atomic库来保证变量复合操作的原子性- 读写
volatile变量会导致变量从主存读写,从主存读写比从 CPU 缓存读写更加昂贵;访问一个volatile变量会禁止指令重排,而指令重排是一种提升性能的技术。因此,应当只在需要保证变量可见性的情况下,才使用volatile变量,以免影响程序性能
参考资料: