1. volatile的特性
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)
- 禁止进行指令重排序。(实现有序性)
- volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。
2. Java内存模型
在Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。在此之前,主流程序语言(C/C++等)直接使用物理硬件和操作系统的内存模型(可以理解为类似于直接使用了硬件标准),都或多或少的在不同的平台有着不一样的执行结果。
Java内存模型的主要目标是定义程序中各个变量的访问规则,即变量在内存中的存储和从内存中取出变量这样的底层细节。其规定了所有变量都存储在主内存,每个线程还有自己的工作内存,线程读写变量时需先复制到工作内存,执行完计算操作后再回写到主内存,每个线程还不能访问其他线程的工作内存。大致示意图如下:
图三我们可以理解为和图二表达的是一个意思,工作内存可以看成是CPU高速缓存、寄存器的抽象,主内存可以看成就是物理硬件中主内存的抽象,图二这个模型会存在缓存一致性问题,图三同样也会存在缓存一致性问题。
另外,为了获得较好的执行性能,Java内存模型并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度,也没有限制编译器对指令进行重排序。也就是说,在Java内存模型中,还会存在指令重排序的问题。
Java语言又是怎么来解决这两个问题的呢?就是通过volatile这个关键字来解决缓存一致性和指令重排问题,volatile作用就是确保可见性和禁止指令重排。
3. volatile的使用
咱们先来写一段经典的单例模式代码来看看。
public class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) { //第一次检查
synchronized (Singleton.class) { //加锁
if (instance == null) { //第二次检查
instance = new Singleton(); //问题的根源出在这里
}
}
}
return instance;
}
public static void main(String[] args) {
Singleton.getInstance();
}
}
上述代码在多线程环境下会有一个问题:在线程执行到“第一次检查”时,读取到的instance不为null,但是instance引用对象有可能还没有完成初始化。
主要原因是重排序:重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。
在new Singleton()时,这一行代码可以分解为3个操作:
memory = allocate(); // 1:分配对象的内存空间
ctorInstance(memory); // 2:初始化对象
instance = memory; // 3:设置instance指向刚分配的内存地址
代码中的2和3之间,可能会被重排序。例如:
memory = allocate(); // 1:分配对象的内存空间
instance = memory; // 3:设置instance指向刚分配的内存地址
// 注意,此时对象还没有被初始化!
ctorInstance(memory); // 2:初始化对象
这在单线程环境下没有问题,但是多线程环境下就可能出现B线程看到一个还没有被A线程初始化完的对象。从而导致B线程判断出instance不为空,但是接下来B线程访问找个对象实例时又没有初始化完。
所以只需要做一点小的修改(把instance声明为volatile型),就可以实现线程安全的延迟初始化。因为被volatile关键字修饰的变量是被禁止重排序的。