Volatile关键字
volatile关键字的作用是实现了共享变量在线程之间的可见性和防止指令的重排序。
volatile分析
在并发编程中有这样三个基本概念:
原子性
一个或者多个操作在执行过程中不会被打断。要么全部执行完毕,要么就全不执行。例如: i = x + 1 ; 这就不是一个原子操作。因为要执行上面的代码需要 首先读取X的值 , 然后把X的值加1 , 再将结果赋值给 i。
volatile并不能保证操作的原子性。
可见性
多个线程使用同一共享变量时,当一个线程修改了这个值,其他线程能立即感知到。
java内存模型
我们可以通过计算机的系统结构来理解java内存模型。
首先,我们都知道在计算机中数据都存储在硬盘中,当程序运行时,运行时的数据则存储在内存中,由CPU来执行指令。
由于cpu执行指令时需要到内存来读取数据,但是IO速度没有CPU执行指令的速度快,所以现在的计算机在内存和CPU之间增加啊了CPU高速缓存来解决数据IO慢的问题。缓存是CPU核心独有的,这样就带来了数据一致性的问题。
- 数据一致性问题
我们把CPU理解为正在运行的线程,那么cpu缓存就是线程的本地内存。线程之间的共享变量存储在主内存中,每一个线程的本地内存存储了该变量副本用以读写操作。本地内存是 JMM 的一个抽象概念,并不真实存在。
当本地内存A 和 本地内存B 同时读取了共享变量X。线程A操作本地内存A中的共享变量X副本,并修改其值;线程B操作其本地内存B中的共享变量X副本,也修改了值。当操作完成后线程AB各自将其本地内存的共享变量X副本回写到主内存时,就会发生冲突。
volatile关键字修饰了共享变量后,在汇编层面观察发现对共享变量进行写操作时会增加Lock指令前缀,它主要由两个作用:
- 将当前处理器缓存行回写到主内存
- 回写操作会使其他处理器缓存该内存地址失效(重新到主内存读取数据)。
有序性
程序执行的顺序按照代码的先后顺序执行。volatile可以保证一定的有序性,防止指令重排序带来线程安全问题。最经典的例子就是双重检查锁。
指令重排序
对于指令重排序,简单来说,需要满足下面两个条件:
- 单线程环境下不会改变程序运行结果
- 不存在数据依赖关系。
需要注意一点:对于try-catch的重排序,为满足不改变程序运行结果条件,程序会在catch 语句中插入错误代偿代码。
双重检查锁
public class Singleton {
private volatile static Singleton singleton;
/*构造方法私有化,不允许在类外构造实例*/
private Singleton(){
};
public static Singleton getInstance() {
/*已经实例化,直接返回对象*/
if(singleton == null){
/*实例化一个对象需要加class对象锁*/
synchronized (Singleton.class) {
/*判断变量没有被实例化(防止多个线程同时竞争,依次进行实例化)*/
if (singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
看上面的代码, 感觉volatile关键字似乎并没有什么用处。 实际上,新建一个对象是分为下面几个步骤的。
- 在堆中分配内存空间:
- 在堆中对对象进行一系列的初始化;
- 将内存地址赋值给对象引用。
值得注意的是,如果没有volatile关键字修饰,上面的第二步和第三步的顺序不是固定的,有可能是先把开辟好的内存地址赋值给对象引用,然后才去初始化对象。 那这样,我们在某一时刻拿到的对象引用可能是未被初始化好的对象。