Volatile
为了平衡 CPU、内存和 IO 设备之间的读写速度,充分利用 CPU 的高性能,我们的计算机体系结构、操作系统以及编译程序都做了很多的优化:
- CPU 增加了高速缓存来平衡 CPU 和内存之间的速度差异,这也就导致
可见性
问题 - 操作系统增加了进程、线程来分时复用 CPU,进而平衡 CPU 和 IO 设备之间的速度,这就带来了
原子性
问题 - 编译程序优化指令执行顺序,使得缓存能够更加充分的利用。这就带来了
有序性
问题。
Volatile 是 Java 的一个关键字,他可以防止重排序,解决有序性问题,还可以通过缓存一致性协议实现可见性
Volatile 可见性实现
可见性的问题主要指的就是,一个线程修改了共享变量的值,另一个内存却看不到。
对一个 volatile 变量的赋值程序通过反编译可以看到,在 volatile 修饰的共享变量进行写操作的时候会多出 lock 前缀的指令
public class Test {private volatile int a;public void update() {a = 1;}public static void main(String[] args) {Test test = new Test();test.update();}
}
0x0000000002951563: and $0xffffffffffffff87,%rdi0x0000000002951567: je 0x00000000029515f80x000000000295156d: test $0x7,%rdi0x0000000002951574: jne 0x00000000029515bd0x0000000002951576: test $0x300,%rdi0x000000000295157d: jne 0x000000000295159c0x000000000295157f: and $0x37f,%rax0x0000000002951586: mov %rax,%rdi0x0000000002951589: or %r15,%rdi0x000000000295158c: lock cmpxchg %rdi,(%rdx) //在 volatile 修饰的共享变量进行写操作的时候会多出 lock 前缀的指令0x0000000002951591: jne 0x0000000002951a150x0000000002951597: jmpq 0x00000000029515f80x000000000295159c: mov 0x8(%rdx),%edi0x000000000295159f: shl $0x3,%rdi0x00000000029515a3: mov 0xa8(%rdi),%rdi0x00000000029515aa: or %r15,%rdi
lock 前缀的指令在多核处理器下会触发两件事情:
- 将当前处理器缓存行数据写入内存
- 写回内存的操作会让其他处理器的缓存中缓存了该地址的数据的缓存失效
对于普通的写操作,处理器不直接和内存进行通信,而是直接将数据写进缓存中,但并不知道什么时候写进内存中。
对于一个 volatile 变量的写操作,JVM 会向处理器发送一条 lock 指令,将这个变量所在的缓存行的数据写回内存中。
为了让各个处理器的缓存是一致的,实现了缓存一致性协议(EMSI),每个处理器会在总线上嗅探自己的缓存是不是过期了,档处理器发现自己的缓存有过期的,则将自己对应的缓存行状态设置为无效,当处理器重新读取这个数据的时候,会从内存中重新读取。
Volatile 有序性实现
happen-before 规则中有一条是 volatile 变量规则:对于一个 volatile 域的写,总是 happen-before 于任意后续对这个 volatile 的读
volatile 可以禁止指令的重排序,JMM提供了内存屏障来阻止重排序
JMM 提供了四种内存屏障:
- StoreStore 屏障:禁止上面的普通写和下面的 volatile 写重排序;
- StoreLoad 屏障:禁止上面的 Volatile 写和下面可能存在的 Volatile 读/写重排序;
- LoadLoad 屏障:禁止下面所有的普通读操作和上面的 Volatile 读重排序
- LoadStore 屏障:禁止下面所有的普通写操作和上面的 Volatile 读重排序
对于编译器来说,发现一个最优布置来最小化插入屏障的总数是不可能的,为此 JMM 采取了保守策略:
- 对于每个 Volatile 写操作,在前面插入一个 StoreStore 屏障
- 对于每个 Volatile 写操作,在后面插入一个 StoreLoad 屏障
- 对于每个 Volatile 读操作,在后面插入一个 LoadLoad 屏障
- 对于每个 Volatile 读操作,在后面插入一个 LoadStore 屏障