volatile是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制:synchronized 同步块(或方法)和 volatile 变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。
————————————————
volatile的特性
- 禁止指令重排序;
- 保证共享变量在多线程之间的可见性;
- 不保证操作的原子性(原子性操作是指: 此操作是逻辑上只需要执行一步,即可完成的最小的可执行单元,要么没执行、要么执行完成、不存在执行了一半的情况);
volatile的原理
加入volatile关键字之后,产生汇编代码之后,在该位置会有一个lock前置指令,相当于内存屏障,该屏障提供三个功能:
1:确保指令重排序时,不会将屏障前的操作排到屏障后,也不会将后边的操作排到屏障前。
2:强制对CPU缓存的修改立即写入内存;
3:如果是写操作,让其他CPU对应的缓存无效;
适用场景
- 轻量级的“读-写锁”策略:一线程写多线程读的变量
- 状态标记:动态修改标记的状态对线程接下来的逻辑进行控制
- 单例模式(双检查锁机制)
使用volatile的几个场景
状态标记
volatile boolean flag = false;
//线程1
while(!flag){
doSomething();
}
//线程2
public void setFlag() {
flag = true;
}
//根据状态标记,终止线程。
单例模式中的double check
class Singleton {
private volatile static Singleton instace;
private Singleton() {}
public static Singleton getInstance(){
//第一次null检查 ,利用volatile的线程间可见性,不需要加锁,性能提高
if(instance == null){
synchronized(Singleton.class) { //同步锁,阻塞其他线程
//第二次null检查,以保证不会创建重复的实例
if(instance == null){
instance = new Singleton(); // 禁止重排序
}
}
}
return instance;
}
}
为什么要使用volatile 修饰instance?
instance = new Singleton();
这并不是一个原子操作,编译成字节码指令一般是含有这两条指令:new,invokespecial(new指令顺序先于invokespecial指令),其中new用来分配对象内存空间并初始化默认值并返回堆对象的引用,而invokespecial指令用来调用对象自定义初始化方法,
这条语句instance = new Singleton(); 可以分解为三个步骤(正常指令顺序):
memory = allocate();
// 给 instance分配内存空间<init>(memory);
// 初始化对象 对应invokespecial调用对象自定义初始化方法(调用 Singleton 的构造函数来初始化成员变量)instance = memory ;
// 将instance对象指向刚分配的内存空间地址引用(执行完这步 instance 就为非 null 了)。
在 JVM 的编译器中允许2和3之间的重排序(处理器重排序)。也就是说上面的第2步和第2步的顺序是不能保证的。如果3排在2的前面,则在3执行完毕(这时instance已经不是null 了)、2未执行之前(还没有完成对象的初始化),此时其他线程读
操作获取的将是未完全初始化的实例。而volatile通过禁止
2和3的重排序
而避免这种情况(3为volatile写)。