前言
在 Java 并发编程中, volatile 关键字有着至关重要的作用, 在面试中也常常会是必备的一个问题. 本文将会介绍 volatile 关键字的作用以及其实现原理.
volatile 作用
volatile 在并发编程中扮演着重要的角色, volatile 是轻量级的 synchronized,volatile 关键字有两个作用:
1) 保证共享变量的可见性
可见性的意思是当一个线程修改一个共享变量时, 另外一个线程能读到这个修改的值. 笔者此前一篇文章 Java 并发编程: Java 内存模型 JMM 中有说到, Java 内存模型中有主内存和本地内存之分, 本地内存持有共享变量的一份副本, 线程对共享变量的修改是先修改本地内存的副本, 然后再回写到主内存中去.
可能存在这样的情况, 线程 A 和线程 B 同时去修改一个共享变量 C, 假设线程 A 先对共享变量 C 做了修改, 而此时线程 B 却没能及时感知到共享变量 C 已经发生了改变, 紧接着 B 对本地过期的副本数据进行了修改, 这造成了共享变量的不可见问题.
而使用了 volatile 关键字修改的共享变量, 当线程修改了共享变量之后, 会立马刷新到主内存中, 并且会使其他线程缓存了该地址的数据失效, 这就保证了线程之间共享变量的可见性.
2) 防止指令重排序
volatile 关键字的另外一个作用就是防止指令重排序. 代码在实际执行过程中, 并不全是按照编写的顺序进行执行的, 在保证单线程执行结果不变的情况下, 编译器或者 CPU 可能会对指令进行重排序, 以提高程序的执行效率. 但是在多线程的情况下, 指令重排序可能会造成一些问题, 最常见的就是双重校验锁单例模式:
- public class SingletonSafe {
- private static volatile SingletonSafe singleton;
- private SingletonSafe() {
- }
- public static SingletonSafe getSingleton() {
- if (singleton == null) {
- synchronized (SingletonSafe.class) {
- if (singleton == null) {
- singleton = new SingletonSafe();
- }
- }
- }
- return singleton;
- }
- }
如果没有使用 volatile 关键字, 则可能会出现其他线程获取了一个未初始化完成的 singleton 对象, 具体原因笔者不在这里赘述了, 有兴趣的同学可以搜索一下 "double checked locking with delay initialization" 学习下, 笔者后续有时间再写篇文章分析下.
volatile 实现原理
1) 可见性实现原理
对于 volatile 关键字修饰的变量, 当对 volatile 变量进行写操作的时候, JVM 会向处理器发送一条 lock 前缀的指令, 将这个缓存中的变量回写到系统主存中. 但是就算写回到内存, 如果其他处理器缓存的值还是旧的, 再执行计算操作就会有问题, 所以在多处理器下, 为了保证各个处理器的缓存是一致的, 就会实现缓存一致性协议.
缓存一致性协议: 每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了, 当处理器发现自己缓存行对应的内存地址被修改, 就会将当前处理器的缓存行设置成无效状态, 当处理器要对这个数据进行修改操作的时候, 会强制重新从系统内存里把数据读到处理器缓存里.
所以, 如果一个变量被 volatile 所修饰的话, 在每次数据变化之后, 其值都会被强制刷入主存. 而其他处理器的缓存由于遵守了缓存一致性协议, 也会把这个变量的值从主存加载到自己的缓存中. 这就保证了一个 volatile 在并发编程中, 其值在多个缓存中是可见的.
2) 防止指令重排序实现原理
volatile 防止指令重排序是通过内存屏障来实现的. 内存屏障分为如下三种:
Store Barrier
Store 屏障, 是 x86 的 "sfence" 指令, 强制所有在 store 屏障指令之前的 store 指令, 都在该 store 屏障指令执行之前被执行.
Load Barrier
Load 屏障, 是 x86 上的 "ifence" 指令, 强制所有在 load 屏障指令之后的 load 指令, 都在该 load 屏障指令执行之后被执行
Full Barrier
Full 屏障, 是 x86 上的 "mfence" 指令, 复合了 load 和 save 屏障的功能.
Java 内存模型中 volatile 变量在写操作之后会插入一个 store 屏障, 在读操作之前会插入一个 load 屏障. 一个类的 final 字段会在初始化后插入一个 store 屏障, 来确保 final 字段在构造函数初始化完成并可被使用时可见. 也正是 JMM 在 volatile 变量读写前后都插入了内存屏障指令, 进而保证了指令的顺序执行.
来源: http://developer.51cto.com/art/201909/602458.htm