可见性
- 多个线程并发读写一个共享变量的时候,有可能某个线程修改了变量的值,但是其他的线程看不到,也就是对其他线程不可见
工作原理
- 主要作用是保证可见性以及有序性
- 不能保证原子性
volatile 怎么保证可见性和有序性
内存可见性
- CPU的内存访问很慢,所以CPU有几层的高速缓存,加速内存访问速度
- Java的内存模型对上述又进行了一些列的抽象,JMM(java内存模型)规定所有的变量都在存在主内存的,每个线程又包含自己的工作内存
- 变量在工作内存中修改以后,就会强制将工作内存中的刷回主内存,主内存的变量值立马变成最新的值
- 其余线程中工作内存中的变量缓存直接强制失效过期,不允许直接读取和使用,当线程再次准备使用的时候,会在主内存中直接读取最新的值
有序性
- JMM是允许编译器和处理器对指令重排序的,但是规定了as-if-serial语义,即不管怎么重排序,程序的执行结果不能改变
- 针对多线程出现的问题,加上volatile 会禁止重排序,可以确保程序的有序性
原子性
- volatile虽然不能保证原子性,但是在某一些条件下,还是能提供原子性的
- 如读 64 位数据类型,像 long 和 double 都不是原子的,但 volatile 类型的 double 和 long 就是原子的。
底层时间的机制
- 如果把加入volatile关键字的代码和未加入volatile关键字的代码都生成汇编代码,会发现加入volatile关键字的代码会多出一个lock前缀的指令
- lock前缀指令实际相当于一个内存屏障
- 内存屏障提供了一下的功能
- 重排序时,不能把后面的指令重排序到内存屏障之前的位置
- 使得本CPU的Cacahe写入内存
- 写入动作也会引起别的CPU或者别的内核无效化其Cache,相当于让新写入的值对别的线程可见。
应用举例
1. 状态量标记
int a = 0;volatile bool flag = false;public void write() { a = 2; //1 flag = true; //2}public void multiply() { if (flag) { //3 int ret = a * a;//4 }}复制代码
2.单例模式的实现,典型的双重检查锁定(DCL)
class Singleton{ private volatile static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if(instance==null) { synchronized (Singleton.class) { if(instance==null) instance = new Singleton(); } } return instance; }}复制代码
这是一种懒汉的单例模式,使用时才创建对象,而且为了避免初始化操作的指令重排序,给instance加上了volatile。
相关的面试题目
1、Java 中能创建 volatile 数组吗?
- 能,不过创建的是一个指定数组的引用,而不是整个数据,如果改变引用指向的数据,将会受到volatile的保护,但是如果多个线程同时改变数据的元素,volatile标识符就不能祈祷之前的保护作用了
2、volatile 能使得一个非原子操作变成原子操作吗?
- 一个典型的例子是在类中的有一个long类型的成员变量,如果你知道该成员变量会被多个线程访问,那么最好将这个成员变量设置为volatile,因为java中读取long类型不是院子的,需要分成两步,如果一个线程正在修改该long变量的值,另一个此案城可能只能看到该值的一般(前32为)。但是对一个 volatile 型的 long 或 double 变量的读写是原子。