volatile 变量自身的特性
- 可见性:对于一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。(类似 happens-before规则)
- 原子性:对任意单个volatile变量的读/写具有原子性,但类似volatile++ 这种复合操作不具有原子性。
当写一个volatile变量时,JMM会把该线程对于的本地内存中的共享变量值刷新到主内存。
当读取一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
class VolatileExample {
int a = 0;
volatile boolean flag = false;
public void writer(){
a = 1; // 1
flag = true; // 2
}
public void reader(){
if(flag) { // 3
int i = a; //4
... ...
}
}
}
当线程A 执行 writer() 时(写flag变量后),本地内存A中被线程A更新过的两个共享变量(a、flag)的值会被刷新到主内存。此时本地内存A和主内存中的共享变量是一致的。
当线程B 执行reader() 时,由于线程A更新过flag,导致本地内存B包含的值已经被置为无效。此时,线程B必须从主内存中读取共享变量。线程B的读取操作将导致本地内存B与主内存中的共享变量的值保持一致。
volatile 内存语义的实现
volatile 重排序规则
- 当第二个操作数是volatile写时,不管第一个操作是扫描,都不能重排序。(确保volatile写之前的操作不会被编译器重排序到volatile写之后)
- 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。(确保volatile读之后的操作不会被编译器重排序到volatile之前)
- 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
JMM内存屏障插入策略
- 在每个volatile写操作的前面插入一个StoreStore 屏障
- 在每个volatile写操作的后面插入一个StoreLoad 屏障
- 在每个volatile读操作的后面插入一个LoadLoad 屏障
- 在每个volatile读操作的后面插入一个LoadStore 屏障
屏障类型
屏障类型 | 指令示例 | 说明 |
---|---|---|
LoadLoad 屏障 | Load1 ;LoadLoad 屏障;Load2 | 确保Load1数据的装载先于Load2以及后面装置指令的装载(防止重排序) |
StoreStore 屏障 | Store1;StoreStore 屏障;Store2 | 确保Store1数据对其他处理器可见(刷新到主内存)先于Store2以及后面的存储指令操作 |
LoadStore 屏障 | Load1;LoadStore 屏障;Store2 | 确保Load1数据装载先于Store2以及后续的存储指令刷新到内存 |
StoreLoad 屏障 | Store1;StoreLoad 屏障;Load2 | 确保Store1数据对其他处理器变得可见(刷新到主内存)先于Load2以及后续的装载指令操作。该屏障会使在它之前的所有内存访问指令(存储和装载)完成后,才执行该屏障之后的内存访问指令 |
例如: 在一个volatile写操作前面插入 StoreStore屏障,会让其前面的所有普通操作已经对任意处理器可见(写入主内存),在一个volatile写操作后面插入 StoreLoad屏障,会让该volatile写操作先于后面可能有的volatile写/读执行(防止重排序)。
volatile 如何来保证可见性?
当有 volatile 变量修饰的共享变量进行写操作时会多出一行汇编代码。
lock add1 .. ...
通过查 IA-32 架构软件开发者手册可以知道,lock 前缀的指令在多核处理器下会发生 两件事。
- 将当前处理器缓存行的数据写回到系统内存。
- 这个写回内存的操作会使其他cpu里缓存了该内存地址的数据无效。
为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1、L2 或其他)后再进行操作,但操作完不知道何时会写到内存。如果声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多核处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检测自己缓存的值是不是过期了,但处理器发现自己的缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读取到处理器缓存。
volatile 的两条实现原则
-
Lock 前缀指令会引起处理器缓存回写到内存。
-
一个处理器的缓存回写到内存会导致其他处理器的缓存无效。