volatile关键字
一、volatile保证内存可见性
jvm规定所有变量数据需要存放在主内存中,同时各线程又有自己的工作内存(用来做高速缓存)。数据由于cpu与内存速度上的差异,所以线程工作的时候,不是直接操作主内存的数据,而是将数据复制主内存的一份副本到线程自己的工作内存中,操作完再更新主内存。
线程工作.png
如图,Thread A将i从主内存复制到自己的变量中,Thread B也复制了一份i。这时候Thread A对i进行了修改,i修改后的值为5,去更新主内存i的值。但是Thread B并没有再次读取主内存的值,而是直接使用自己工作内存的值i=1,这就导致数据不一致了。
那么如何解决呢?就需要使用volatile了。volatile修饰的变量保证内存可见性,所谓内存可见性就是当一个变量被修改时会立即更新到主内存中。i修改为5了,同时其他线程中的变量i失效了,必须从主内存中重新读取,这时候读取的就是新值5了。
二、volatile特性
- 内存可见性。
- 防止指令重排。
内存可见性已经讲过了,下面讲一下这个防止指令重排。
int a=1 ; // 1
int b=2 ; // 2
int c= a+b ; // 3
上面这段代码我们想象中执行的顺序时1-》2-》3。但其实步骤1和步骤2的关系不大,所以jvm优化后可能执行的顺序是2-》1-》3。jvm指令优化是在不改变最终结果的情况下进行指令重排,在单线程情况下是没有关系的,因为不影响最终结果。而多线程的情况下,可能会发生意想不到的结果。用volatile修饰后,就会按顺序执行了。
三、volatile使用场景
3.1 内存可见性,比如状态标识量
public class Sale {
private volatile boolean openFlag;
public void setOpenFlag(boolean flag){
this.openFlag = flag;
}
public void saleGoods(){
if(openFlag){
//开店营业
...
}else{
//关店调整
...
}
}
}
openFlag用volatile修饰,保证获取到的都是最新值。
3.2 防止指令重排,这个比较经典的就是单例模式
/**
* <h1>单例模式</h1>
*/
public class Singleton {
private volatile static Singleton singleton;
/**
* 构造器私有化
*/
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
singleton用volatile修饰,防止指令重排。因为 singleton = new Singleton()会分解为三个操作:
1.分配内存
2.初始化
3.变量指向分配的内存地址
如果不用volatile修饰,因为步骤2和3没有关系,所以可能jvm执行的顺序是1-》3-》2 。所以线程A在执行后singleton已经指向新分配的内存地址,但还没执行初始化这一步。线程B这时候进来了,判断singleton不为null,就返回了,但这时候singleton是未初始化的,这就有问题了。