字分裂
当你的 Java 数据类型足够大(在 Java 中 long
和 double
类型都是 64 位),(32 位 JDK)写入变量的过程会分两步进行,就会发生 Word tearing
(字分裂)情况。 JVM 被允许将 64 位数量的读写作为两个单独的 32 位操作执行,这增加了在读写过程中发生上下文切换的可能性,因此其他任务会看到不正确的结果。这被称为 Word tearing
(字分裂),因为你可能只看到其中一部分修改后的值。基本上,任务有时可以在第一步之后但在第二步之前读取变量,从而产生垃圾值(对于例如 boolean
或 int
类型的小变量是没有问题的;任何 long
或 double
类型则除外)
在缺乏任何其他保护的情况下,用 volatile
修饰符定义一个 long
或 double
变量,可阻止字分裂情况。然而,如果使用 synchronized
或 java.util.concurrent.atomic
库之一保护这些变量,则 volatile
将被取代。此外,volatile
不会影响到增量操作并不是原子操作的事实(例如 i++
)
可见性
在一个多线程的应用中,线程在操作非 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
变量,以免影响程序性能
参考资料: